golem-vm-provider 0.1.54__tar.gz → 0.1.55__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.
Files changed (45) hide show
  1. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/PKG-INFO +4 -1
  2. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/README.md +3 -0
  3. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/payments/monitor.py +27 -7
  4. golem_vm_provider-0.1.55/provider/service.py +148 -0
  5. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/pyproject.toml +1 -1
  6. golem_vm_provider-0.1.54/provider/service.py +0 -86
  7. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/__init__.py +0 -0
  8. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/api/__init__.py +0 -0
  9. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/api/models.py +0 -0
  10. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/api/routes.py +0 -0
  11. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/config.py +0 -0
  12. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/container.py +0 -0
  13. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/data/deployments/l2.json +0 -0
  14. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/__init__.py +0 -0
  15. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/advertiser.py +0 -0
  16. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/golem_base_advertiser.py +0 -0
  17. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/golem_base_utils.py +0 -0
  18. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/multi_advertiser.py +0 -0
  19. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/resource_monitor.py +0 -0
  20. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/resource_tracker.py +0 -0
  21. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/discovery/service.py +0 -0
  22. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/main.py +0 -0
  23. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/network/port_verifier.py +0 -0
  24. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/payments/blockchain_service.py +0 -0
  25. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/payments/stream_map.py +0 -0
  26. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/security/ethereum.py +0 -0
  27. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/security/faucet.py +0 -0
  28. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/security/l2_faucet.py +0 -0
  29. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/utils/__init__.py +0 -0
  30. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/utils/ascii_art.py +0 -0
  31. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/utils/logging.py +0 -0
  32. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/utils/port_display.py +0 -0
  33. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/utils/pricing.py +0 -0
  34. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/utils/retry.py +0 -0
  35. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/utils/setup.py +0 -0
  36. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/__init__.py +0 -0
  37. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/cloud_init.py +0 -0
  38. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/models.py +0 -0
  39. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/multipass.py +0 -0
  40. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/multipass_adapter.py +0 -0
  41. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/name_mapper.py +0 -0
  42. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/port_manager.py +0 -0
  43. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/provider.py +0 -0
  44. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/proxy_manager.py +0 -0
  45. {golem_vm_provider-0.1.54 → golem_vm_provider-0.1.55}/provider/vm/service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.54
3
+ Version: 0.1.55
4
4
  Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
5
5
  Keywords: golem,vm,provider,cloud,decentralized
6
6
  Author: Phillip Jensen
@@ -510,6 +510,7 @@ sequenceDiagram
510
510
  participant DS as Discovery Service
511
511
 
512
512
  P->>RT: Initialize
513
+ P->>RT: Sync with existing VMs
513
514
  RT->>AD: Register Callback
514
515
  loop Every 4 minutes
515
516
  AD->>RT: Get Resources
@@ -519,6 +520,8 @@ sequenceDiagram
519
520
  end
520
521
  ```
521
522
 
523
+ On startup, the provider syncs the resource tracker with all VMs currently running on the host (via Multipass). This ensures advertisements reflect already-allocated CPU, RAM, and storage after restarts, preventing false “outdated advertisement” updates when existing VMs are consuming resources. The sync is based on actual VMs present, independent of any still-open payment streams.
524
+
522
525
  ### Monitoring
523
526
 
524
527
  The provider includes comprehensive logging:
@@ -465,6 +465,7 @@ sequenceDiagram
465
465
  participant DS as Discovery Service
466
466
 
467
467
  P->>RT: Initialize
468
+ P->>RT: Sync with existing VMs
468
469
  RT->>AD: Register Callback
469
470
  loop Every 4 minutes
470
471
  AD->>RT: Get Resources
@@ -474,6 +475,8 @@ sequenceDiagram
474
475
  end
475
476
  ```
476
477
 
478
+ On startup, the provider syncs the resource tracker with all VMs currently running on the host (via Multipass). This ensures advertisements reflect already-allocated CPU, RAM, and storage after restarts, preventing false “outdated advertisement” updates when existing VMs are consuming resources. The sync is based on actual VMs present, independent of any still-open payment streams.
479
+
477
480
  ### Monitoring
478
481
 
479
482
  The provider includes comprehensive logging:
@@ -56,21 +56,41 @@ class StreamMonitor:
56
56
  logger.warning(f"stream {stream_id} lookup failed: {e}")
57
57
  continue
58
58
  # Stop VM if remaining runway < threshold
59
- remaining = max(s["stopTime"] - now, 0)
59
+ remaining = max(int(s["stopTime"]) - int(now), 0)
60
60
  logger.debug(
61
61
  f"stream {stream_id} for VM {vm_id}: start={s['startTime']} stop={s['stopTime']} "
62
62
  f"rate={s['ratePerSecond']} withdrawn={s['withdrawn']} halted={s['halted']} remaining={remaining}s"
63
63
  )
64
- if self._get("STREAM_MONITOR_ENABLED", False) and remaining < int(self._get("STREAM_MIN_REMAINING_SECONDS", 0)):
65
- logger.info(f"Stopping VM {vm_id} due to low stream runway ({remaining}s)")
64
+ # If stream is force-halted, delete immediately to free all resources
65
+ if bool(s.get("halted")):
66
+ logger.info(
67
+ f"Deleting VM {vm_id} due to halted stream (id={stream_id}, now={now})"
68
+ )
69
+ try:
70
+ await self.vm_service.delete_vm(vm_id)
71
+ except Exception as e:
72
+ logger.warning(f"delete_vm failed for {vm_id}: {e}")
73
+ try:
74
+ await self.stream_map.remove(vm_id)
75
+ except Exception as e:
76
+ logger.debug(f"failed to remove vm {vm_id} from stream map: {e}")
77
+ continue
78
+
79
+ # Only stop a VM when runway is completely empty
80
+ if remaining == 0:
81
+ logger.info(
82
+ f"Stopping VM {vm_id} as stream runway is exhausted (id={stream_id}, now={now}, stop={s.get('stopTime')})"
83
+ )
66
84
  try:
67
85
  await self.vm_service.stop_vm(vm_id)
68
86
  except Exception as e:
69
87
  logger.warning(f"stop_vm failed for {vm_id}: {e}")
70
- else:
71
- logger.debug(
72
- f"VM {vm_id} stream {stream_id} healthy (remaining={remaining}s, threshold={self._get('STREAM_MIN_REMAINING_SECONDS', 0)}s)"
73
- )
88
+ continue
89
+
90
+ # Otherwise, do not stop; just log health and consider withdrawals
91
+ logger.debug(
92
+ f"VM {vm_id} stream {stream_id} healthy (remaining={remaining}s)"
93
+ )
74
94
  # Withdraw if enough vested and configured
75
95
  if self._get("STREAM_WITHDRAW_ENABLED", False) and self.client:
76
96
  vested = max(min(now, s["stopTime"]) - s["startTime"], 0) * s["ratePerSecond"]
@@ -0,0 +1,148 @@
1
+ import asyncio
2
+ from fastapi import FastAPI
3
+
4
+ from .utils.logging import setup_logger
5
+ from .vm.service import VMService
6
+ from .discovery.service import AdvertisementService
7
+ from .utils.pricing import PricingAutoUpdater
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+
12
+ class ProviderService:
13
+ """Service for managing the provider's lifecycle."""
14
+
15
+ def __init__(self, vm_service: VMService, advertisement_service: AdvertisementService, port_manager):
16
+ self.vm_service = vm_service
17
+ self.advertisement_service = advertisement_service
18
+ self.port_manager = port_manager
19
+ self._pricing_updater: PricingAutoUpdater | None = None
20
+ self._stream_monitor = None
21
+
22
+ async def setup(self, app: FastAPI):
23
+ """Setup and initialize the provider components."""
24
+ from .config import settings
25
+ from .utils.ascii_art import startup_animation
26
+ from .security.faucet import FaucetClient
27
+
28
+ try:
29
+ # Display startup animation
30
+ await startup_animation()
31
+
32
+ logger.process("🔄 Initializing provider...")
33
+
34
+ # Setup directories
35
+ self._setup_directories()
36
+
37
+ # Initialize services
38
+ await self.port_manager.initialize()
39
+ await self.vm_service.provider.initialize()
40
+
41
+ # Before starting advertisement, sync allocated resources with existing VMs
42
+ try:
43
+ vm_resources = await self.vm_service.get_all_vms_resources()
44
+ await self.vm_service.resource_tracker.sync_with_multipass(vm_resources)
45
+ except Exception as e:
46
+ logger.warning(f"Failed to sync resources with existing VMs: {e}")
47
+
48
+ # Cross-check running VMs against payment streams. If a VM has no
49
+ # active stream, it is no longer rented: terminate it and free resources.
50
+ try:
51
+ # Only perform checks if payments are configured
52
+ if settings.STREAM_PAYMENT_ADDRESS and not settings.STREAM_PAYMENT_ADDRESS.lower().endswith("0000000000000000000000000000000000000000") and settings.POLYGON_RPC_URL:
53
+ stream_map = app.container.stream_map()
54
+ reader = app.container.stream_reader()
55
+
56
+ # Use the most recent view of VMs from the previous sync
57
+ vm_ids = list(vm_resources.keys()) if 'vm_resources' in locals() else []
58
+ for vm_id in vm_ids:
59
+ try:
60
+ stream_id = await stream_map.get(vm_id)
61
+ except Exception:
62
+ stream_id = None
63
+
64
+ if stream_id is None:
65
+ reason = "no stream mapped"
66
+ should_terminate = True
67
+ else:
68
+ try:
69
+ ok, msg = reader.verify_stream(int(stream_id), settings.PROVIDER_ID)
70
+ should_terminate = not ok
71
+ reason = msg if not ok else "ok"
72
+ except Exception as e:
73
+ # If verification cannot be performed, be conservative and keep the VM
74
+ logger.warning(f"Stream verification error for VM {vm_id} (stream {stream_id}): {e}")
75
+ should_terminate = False
76
+ reason = f"verification error: {e}"
77
+
78
+ if should_terminate:
79
+ logger.info(
80
+ f"Deleting VM {vm_id}: inactive stream (stream_id={stream_id}, reason={reason})"
81
+ )
82
+ try:
83
+ await self.vm_service.delete_vm(vm_id)
84
+ except Exception as e:
85
+ logger.warning(f"Failed to delete VM {vm_id}: {e}")
86
+ try:
87
+ await stream_map.remove(vm_id)
88
+ except Exception:
89
+ pass
90
+
91
+ # Re-sync after any terminations to ensure ads reflect capacity
92
+ try:
93
+ vm_resources = await self.vm_service.get_all_vms_resources()
94
+ await self.vm_service.resource_tracker.sync_with_multipass(vm_resources)
95
+ except Exception as e:
96
+ logger.warning(f"Post-termination resource sync failed: {e}")
97
+ else:
98
+ logger.info("Payments not configured; skipping startup stream checks")
99
+ except Exception as e:
100
+ logger.warning(f"Failed to reconcile VMs with payment streams: {e}")
101
+
102
+ await self.advertisement_service.start()
103
+ # Start pricing auto-updater; trigger re-advertise after updates
104
+ async def _on_price_updated(platform: str, glm_usd):
105
+ await self.advertisement_service.trigger_update()
106
+ self._pricing_updater = PricingAutoUpdater(on_updated_callback=_on_price_updated)
107
+ asyncio.create_task(self._pricing_updater.start())
108
+
109
+ # Start stream monitor if enabled
110
+ from .container import Container
111
+ from .config import settings as cfg
112
+ if cfg.STREAM_MONITOR_ENABLED or cfg.STREAM_WITHDRAW_ENABLED:
113
+ self._stream_monitor = app.container.stream_monitor()
114
+ self._stream_monitor.start()
115
+
116
+ # Check wallet balance and request funds if needed
117
+ faucet_client = FaucetClient(
118
+ faucet_url=settings.FAUCET_URL,
119
+ captcha_url=settings.CAPTCHA_URL,
120
+ captcha_api_key=settings.CAPTCHA_API_KEY,
121
+ )
122
+ await faucet_client.get_funds(settings.PROVIDER_ID)
123
+
124
+ logger.success("✨ Provider setup complete")
125
+ except Exception as e:
126
+ logger.error(f"Startup failed: {e}")
127
+ await self.cleanup()
128
+ raise
129
+
130
+ async def cleanup(self):
131
+ """Cleanup provider components."""
132
+ logger.process("🔄 Cleaning up provider...")
133
+ await self.advertisement_service.stop()
134
+ await self.vm_service.provider.cleanup()
135
+ if self._pricing_updater:
136
+ self._pricing_updater.stop()
137
+ if self._stream_monitor:
138
+ await self._stream_monitor.stop()
139
+ logger.success("✨ Provider cleanup complete")
140
+
141
+ def _setup_directories(self):
142
+ """Create necessary directories for the provider."""
143
+ from .config import settings
144
+ from pathlib import Path
145
+
146
+ Path(settings.VM_DATA_DIR).mkdir(parents=True, exist_ok=True)
147
+ Path(settings.SSH_KEY_DIR).mkdir(parents=True, exist_ok=True)
148
+ Path(settings.CLOUD_INIT_DIR).mkdir(parents=True, exist_ok=True)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "golem-vm-provider"
3
- version = "0.1.54"
3
+ version = "0.1.55"
4
4
  description = "VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network"
5
5
  authors = ["Phillip Jensen <phillip+vm-on-golem@golemgrid.com>"]
6
6
  readme = "README.md"
@@ -1,86 +0,0 @@
1
- import asyncio
2
- from fastapi import FastAPI
3
-
4
- from .utils.logging import setup_logger
5
- from .vm.service import VMService
6
- from .discovery.service import AdvertisementService
7
- from .utils.pricing import PricingAutoUpdater
8
-
9
- logger = setup_logger(__name__)
10
-
11
-
12
- class ProviderService:
13
- """Service for managing the provider's lifecycle."""
14
-
15
- def __init__(self, vm_service: VMService, advertisement_service: AdvertisementService, port_manager):
16
- self.vm_service = vm_service
17
- self.advertisement_service = advertisement_service
18
- self.port_manager = port_manager
19
- self._pricing_updater: PricingAutoUpdater | None = None
20
- self._stream_monitor = None
21
-
22
- async def setup(self, app: FastAPI):
23
- """Setup and initialize the provider components."""
24
- from .config import settings
25
- from .utils.ascii_art import startup_animation
26
- from .security.faucet import FaucetClient
27
-
28
- try:
29
- # Display startup animation
30
- await startup_animation()
31
-
32
- logger.process("🔄 Initializing provider...")
33
-
34
- # Setup directories
35
- self._setup_directories()
36
-
37
- # Initialize services
38
- await self.port_manager.initialize()
39
- await self.vm_service.provider.initialize()
40
- await self.advertisement_service.start()
41
- # Start pricing auto-updater; trigger re-advertise after updates
42
- async def _on_price_updated(platform: str, glm_usd):
43
- await self.advertisement_service.trigger_update()
44
- self._pricing_updater = PricingAutoUpdater(on_updated_callback=_on_price_updated)
45
- asyncio.create_task(self._pricing_updater.start())
46
-
47
- # Start stream monitor if enabled
48
- from .container import Container
49
- from .config import settings as cfg
50
- if cfg.STREAM_MONITOR_ENABLED or cfg.STREAM_WITHDRAW_ENABLED:
51
- self._stream_monitor = app.container.stream_monitor()
52
- self._stream_monitor.start()
53
-
54
- # Check wallet balance and request funds if needed
55
- faucet_client = FaucetClient(
56
- faucet_url=settings.FAUCET_URL,
57
- captcha_url=settings.CAPTCHA_URL,
58
- captcha_api_key=settings.CAPTCHA_API_KEY,
59
- )
60
- await faucet_client.get_funds(settings.PROVIDER_ID)
61
-
62
- logger.success("✨ Provider setup complete")
63
- except Exception as e:
64
- logger.error(f"Startup failed: {e}")
65
- await self.cleanup()
66
- raise
67
-
68
- async def cleanup(self):
69
- """Cleanup provider components."""
70
- logger.process("🔄 Cleaning up provider...")
71
- await self.advertisement_service.stop()
72
- await self.vm_service.provider.cleanup()
73
- if self._pricing_updater:
74
- self._pricing_updater.stop()
75
- if self._stream_monitor:
76
- await self._stream_monitor.stop()
77
- logger.success("✨ Provider cleanup complete")
78
-
79
- def _setup_directories(self):
80
- """Create necessary directories for the provider."""
81
- from .config import settings
82
- from pathlib import Path
83
-
84
- Path(settings.VM_DATA_DIR).mkdir(parents=True, exist_ok=True)
85
- Path(settings.SSH_KEY_DIR).mkdir(parents=True, exist_ok=True)
86
- Path(settings.CLOUD_INIT_DIR).mkdir(parents=True, exist_ok=True)