golem-vm-provider 0.1.24__py3-none-any.whl → 0.1.27__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.
provider/container.py ADDED
@@ -0,0 +1,84 @@
1
+ import os
2
+ from dependency_injector import containers, providers
3
+ from pathlib import Path
4
+
5
+ from .config import settings
6
+ from .discovery.resource_tracker import ResourceTracker
7
+ from .discovery.golem_base_advertiser import GolemBaseAdvertiser
8
+ from .discovery.advertiser import DiscoveryServerAdvertiser
9
+ from .discovery.service import AdvertisementService
10
+ from .service import ProviderService
11
+ from .vm.multipass_adapter import MultipassAdapter
12
+ from .vm.service import VMService
13
+ from .vm.name_mapper import VMNameMapper
14
+ from .vm.port_manager import PortManager
15
+ from .vm.proxy_manager import PythonProxyManager
16
+
17
+
18
+ class Container(containers.DeclarativeContainer):
19
+ """Dependency injection container."""
20
+
21
+ config = providers.Configuration()
22
+
23
+ resource_tracker = providers.Singleton(ResourceTracker)
24
+
25
+ advertiser = providers.Selector(
26
+ config.ADVERTISER_TYPE,
27
+ golem_base=providers.Singleton(
28
+ GolemBaseAdvertiser,
29
+ resource_tracker=resource_tracker,
30
+ ),
31
+ discovery_server=providers.Singleton(
32
+ DiscoveryServerAdvertiser,
33
+ resource_tracker=resource_tracker,
34
+ ),
35
+ )
36
+
37
+ advertisement_service = providers.Singleton(
38
+ AdvertisementService,
39
+ advertiser=advertiser,
40
+ )
41
+
42
+ vm_name_mapper = providers.Singleton(
43
+ VMNameMapper,
44
+ db_path=Path(settings.VM_DATA_DIR) / "vm_names.json",
45
+ )
46
+
47
+ port_manager = providers.Singleton(
48
+ PortManager,
49
+ start_port=config.PORT_RANGE_START,
50
+ end_port=config.PORT_RANGE_END,
51
+ state_file=providers.Callable(
52
+ os.path.join,
53
+ config.PROXY_STATE_DIR,
54
+ "ports.json"
55
+ ),
56
+ discovery_port=config.PORT,
57
+ skip_verification=config.SKIP_PORT_VERIFICATION,
58
+ )
59
+
60
+ proxy_manager = providers.Singleton(
61
+ PythonProxyManager,
62
+ port_manager=port_manager,
63
+ name_mapper=vm_name_mapper,
64
+ )
65
+
66
+ vm_provider = providers.Singleton(
67
+ MultipassAdapter,
68
+ proxy_manager=proxy_manager,
69
+ name_mapper=vm_name_mapper,
70
+ )
71
+
72
+ vm_service = providers.Singleton(
73
+ VMService,
74
+ provider=vm_provider,
75
+ resource_tracker=resource_tracker,
76
+ name_mapper=vm_name_mapper,
77
+ )
78
+
79
+ provider_service = providers.Singleton(
80
+ ProviderService,
81
+ vm_service=vm_service,
82
+ advertisement_service=advertisement_service,
83
+ port_manager=port_manager,
84
+ )
@@ -1,6 +1,12 @@
1
- from .advertiser import ResourceMonitor, ResourceAdvertiser
1
+ from .advertiser import Advertiser, DiscoveryServerAdvertiser
2
+ from .golem_base_advertiser import GolemBaseAdvertiser
3
+ from .resource_monitor import ResourceMonitor
4
+ from .service import AdvertisementService
2
5
 
3
6
  __all__ = [
7
+ "Advertiser",
8
+ "DiscoveryServerAdvertiser",
9
+ "GolemBaseAdvertiser",
4
10
  "ResourceMonitor",
5
- "ResourceAdvertiser"
11
+ "AdvertisementService",
6
12
  ]
@@ -1,90 +1,71 @@
1
1
  import aiohttp
2
2
  import asyncio
3
3
  import logging
4
- import psutil
5
- from datetime import datetime
6
- from typing import Dict, Optional
4
+ from abc import ABC, abstractmethod
5
+ from typing import Optional
7
6
 
8
7
  from ..config import settings
9
8
  from ..utils.retry import async_retry
9
+ from .resource_tracker import ResourceTracker
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
- class ResourceMonitor:
14
- """Monitor system resources."""
15
-
16
- @staticmethod
17
- def get_cpu_count() -> int:
18
- """Get number of CPU cores."""
19
- return psutil.cpu_count()
20
-
21
- @staticmethod
22
- def get_memory_gb() -> int:
23
- """Get available memory in GB."""
24
- return psutil.virtual_memory().available // (1024 ** 3)
25
-
26
- @staticmethod
27
- def get_storage_gb() -> int:
28
- """Get available storage in GB."""
29
- return psutil.disk_usage("/").free // (1024 ** 3)
30
-
31
- @staticmethod
32
- def get_cpu_percent() -> float:
33
- """Get CPU usage percentage."""
34
- return psutil.cpu_percent(interval=1)
35
-
36
- @staticmethod
37
- def get_memory_percent() -> float:
38
- """Get memory usage percentage."""
39
- return psutil.virtual_memory().percent
40
-
41
- @staticmethod
42
- def get_storage_percent() -> float:
43
- """Get storage usage percentage."""
44
- return psutil.disk_usage("/").percent
45
-
46
- class ResourceAdvertiser:
47
- """Advertise available resources to discovery service."""
13
+ class Advertiser(ABC):
14
+ """Abstract base class for advertisers."""
15
+
16
+ @abstractmethod
17
+ async def initialize(self):
18
+ """Initialize the advertiser."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ async def start_loop(self):
23
+ """Start the advertising loop."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def stop(self):
28
+ """Stop the advertising loop."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ async def post_advertisement(self):
33
+ """Post a single advertisement."""
34
+ pass
35
+
36
+ class DiscoveryServerAdvertiser(Advertiser):
37
+ """Advertise available resources to a discovery service."""
48
38
 
49
39
  def __init__(
50
40
  self,
51
41
  resource_tracker: 'ResourceTracker',
52
42
  discovery_url: Optional[str] = None,
53
43
  provider_id: Optional[str] = None,
54
- update_interval: Optional[int] = None
55
44
  ):
56
45
  self.resource_tracker = resource_tracker
57
46
  self.discovery_url = discovery_url or settings.DISCOVERY_URL
58
47
  self.provider_id = provider_id or settings.PROVIDER_ID
59
- self.update_interval = update_interval or settings.ADVERTISEMENT_INTERVAL
60
48
  self.session: Optional[aiohttp.ClientSession] = None
61
49
  self._stop_event = asyncio.Event()
62
50
 
63
- async def start(self):
64
- """Start advertising resources."""
51
+ async def initialize(self):
52
+ """Initialize the advertiser."""
65
53
  self.session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
66
- # Register for resource updates
67
- self.resource_tracker.on_update(self._post_advertisement)
68
-
69
- # Test discovery service connection with retries
54
+ self.resource_tracker.on_update(
55
+ lambda: asyncio.create_task(self.post_advertisement())
56
+ )
70
57
  try:
71
58
  await self._check_discovery_health()
72
59
  except Exception as e:
73
60
  logger.warning(f"Could not connect to discovery service after retries, continuing without advertising: {e}")
74
61
  return
75
-
62
+
63
+ async def start_loop(self):
64
+ """Start advertising resources in a loop."""
76
65
  try:
77
66
  while not self._stop_event.is_set():
78
- try:
79
- await self._post_advertisement()
80
- except aiohttp.ClientError as e:
81
- logger.error(f"Network error posting advertisement: {e}")
82
- await asyncio.sleep(min(60, self.update_interval))
83
- except Exception as e:
84
- logger.error(f"Failed to post advertisement: {e}")
85
- await asyncio.sleep(min(60, self.update_interval))
86
- else:
87
- await asyncio.sleep(self.update_interval)
67
+ await self.post_advertisement()
68
+ await asyncio.sleep(settings.ADVERTISEMENT_INTERVAL)
88
69
  finally:
89
70
  await self.stop()
90
71
 
@@ -106,19 +87,17 @@ class ResourceAdvertiser:
106
87
  raise Exception(f"Discovery service health check failed: {response.status}")
107
88
 
108
89
  @async_retry(retries=3, delay=1.0, backoff=2.0, exceptions=(aiohttp.ClientError, asyncio.TimeoutError))
109
- async def _post_advertisement(self):
90
+ async def post_advertisement(self):
110
91
  """Post resource advertisement to discovery service."""
111
92
  if not self.session:
112
93
  raise RuntimeError("Session not initialized")
113
94
 
114
95
  resources = self.resource_tracker.get_available_resources()
115
96
 
116
- # Don't advertise if resources are too low
117
97
  if not self.resource_tracker._meets_minimum_requirements(resources):
118
98
  logger.warning("Resources too low, skipping advertisement")
119
99
  return
120
100
 
121
- # Get public IP with retries
122
101
  try:
123
102
  ip_address = await self._get_public_ip()
124
103
  except Exception as e:
@@ -130,7 +109,7 @@ class ResourceAdvertiser:
130
109
  f"{self.discovery_url}/api/v1/advertisements",
131
110
  headers={
132
111
  "X-Provider-ID": self.provider_id,
133
- "X-Provider-Signature": "signature", # TODO: Implement signing
112
+ "X-Provider-Signature": "signature",
134
113
  "Content-Type": "application/json"
135
114
  },
136
115
  json={
@@ -138,7 +117,7 @@ class ResourceAdvertiser:
138
117
  "country": settings.PROVIDER_COUNTRY,
139
118
  "resources": resources
140
119
  },
141
- timeout=aiohttp.ClientTimeout(total=5) # 5 second timeout for advertisement
120
+ timeout=aiohttp.ClientTimeout(total=5)
142
121
  ) as response:
143
122
  if not response.ok:
144
123
  error_text = await response.text()
@@ -159,7 +138,6 @@ class ResourceAdvertiser:
159
138
  if not self.session:
160
139
  raise RuntimeError("Session not initialized")
161
140
 
162
- # Try multiple IP services in case one fails
163
141
  services = [
164
142
  "https://api.ipify.org",
165
143
  "https://ifconfig.me/ip",
@@ -0,0 +1,135 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ from golem_base_sdk import GolemBaseClient, GolemBaseCreate, GolemBaseUpdate, GolemBaseDelete, Annotation
5
+ from .advertiser import Advertiser
6
+ from .golem_base_utils import get_provider_entity_keys
7
+ from ..config import settings
8
+ from ..utils.logging import setup_logger
9
+
10
+ logger = setup_logger(__name__)
11
+
12
+
13
+ class GolemBaseAdvertiser(Advertiser):
14
+ """Advertise available resources to the Golem Base network."""
15
+
16
+ def __init__(self, resource_tracker: "ResourceTracker"):
17
+ self.resource_tracker = resource_tracker
18
+ self.client: Optional[GolemBaseClient] = None
19
+ self._stop_event = asyncio.Event()
20
+
21
+ async def initialize(self):
22
+ """Initialize the advertiser."""
23
+ private_key_hex = settings.ETHEREUM_PRIVATE_KEY.replace("0x", "")
24
+ private_key_bytes = bytes.fromhex(private_key_hex)
25
+ self.client = await GolemBaseClient.create(
26
+ rpc_url=settings.GOLEM_BASE_RPC_URL,
27
+ ws_url=settings.GOLEM_BASE_WS_URL,
28
+ private_key=private_key_bytes,
29
+ )
30
+ self.resource_tracker.on_update(
31
+ lambda: asyncio.create_task(self.post_advertisement())
32
+ )
33
+
34
+ async def start_loop(self):
35
+ """Start advertising resources in a loop."""
36
+ try:
37
+ while not self._stop_event.is_set():
38
+ await self.post_advertisement()
39
+ await asyncio.sleep(settings.ADVERTISEMENT_INTERVAL)
40
+ finally:
41
+ await self.stop()
42
+
43
+ async def stop(self):
44
+ """Stop advertising resources."""
45
+ self._stop_event.set()
46
+ if self.client:
47
+ await self.client.disconnect()
48
+
49
+ async def post_advertisement(self):
50
+ """Post or update resource advertisement on the Golem Base network."""
51
+ if not self.client:
52
+ raise RuntimeError("Golem Base client not initialized")
53
+
54
+ resources = self.resource_tracker.get_available_resources()
55
+ if not self.resource_tracker._meets_minimum_requirements(resources):
56
+ logger.warning("Resources too low, skipping advertisement")
57
+ return
58
+
59
+ ip_address = settings.PUBLIC_IP
60
+ if not ip_address:
61
+ logger.error("Could not get public IP, skipping advertisement")
62
+ return
63
+
64
+ try:
65
+ existing_keys = await get_provider_entity_keys(self.client, settings.PROVIDER_ID)
66
+
67
+ string_annotations = [
68
+ Annotation(key="golem_type", value="provider"),
69
+ Annotation(key="golem_provider_id", value=settings.PROVIDER_ID),
70
+ Annotation(key="golem_ip_address", value=ip_address),
71
+ Annotation(key="golem_country", value=settings.PROVIDER_COUNTRY),
72
+ Annotation(key="golem_provider_name", value=settings.PROVIDER_NAME),
73
+ ]
74
+ numeric_annotations = [
75
+ Annotation(key="golem_cpu", value=resources["cpu"]),
76
+ Annotation(key="golem_memory", value=resources["memory"]),
77
+ Annotation(key="golem_storage", value=resources["storage"]),
78
+ ]
79
+
80
+ if len(existing_keys) > 1:
81
+ logger.warning(f"Found {len(existing_keys)} advertisements. Cleaning up and creating a new one.")
82
+ deletes = [GolemBaseDelete(entity_key=key) for key in existing_keys]
83
+ await self.client.delete_entities(deletes)
84
+ await self._create_advertisement(string_annotations, numeric_annotations)
85
+
86
+ elif len(existing_keys) == 1:
87
+ entity_key = existing_keys[0]
88
+ metadata = await self.client.get_entity_metadata(entity_key)
89
+
90
+ current_annotations = {ann.key: ann.value for ann in metadata.numeric_annotations}
91
+ current_annotations.update({ann.key: ann.value for ann in metadata.string_annotations})
92
+
93
+ # Full comparison of all annotations
94
+ expected_annotations = {ann.key: ann.value for ann in string_annotations}
95
+ expected_annotations.update({ann.key: ann.value for ann in numeric_annotations})
96
+
97
+ # Debugging logs to compare annotations
98
+ logger.info(f"IP address from settings: {ip_address}")
99
+ logger.info(f"Current on-chain annotations: {current_annotations}")
100
+ logger.info(f"Expected annotations based on current config: {expected_annotations}")
101
+
102
+ if sorted(current_annotations.items()) == sorted(expected_annotations.items()):
103
+ logger.info("Advertisement is up-to-date. Waiting for expiration.")
104
+ else:
105
+ logger.info("Advertisement is outdated. Updating.")
106
+ update = GolemBaseUpdate(
107
+ entity_key=entity_key,
108
+ data=b"",
109
+ btl=settings.ADVERTISEMENT_INTERVAL * 2,
110
+ string_annotations=string_annotations,
111
+ numeric_annotations=numeric_annotations,
112
+ )
113
+ await self.client.update_entities([update])
114
+ logger.info(f"Updated advertisement. Entity key: {entity_key}")
115
+
116
+ else: # No existing keys
117
+ await self._create_advertisement(string_annotations, numeric_annotations)
118
+
119
+ except Exception as e:
120
+ logger.error(f"Failed to post or update advertisement on Golem Base: {e}")
121
+
122
+ async def _create_advertisement(self, string_annotations, numeric_annotations):
123
+ """Helper to create a new advertisement."""
124
+ entity = GolemBaseCreate(
125
+ data=b"",
126
+ btl=settings.ADVERTISEMENT_INTERVAL * 2,
127
+ string_annotations=string_annotations,
128
+ numeric_annotations=numeric_annotations,
129
+ )
130
+ receipts = await self.client.create_entities([entity])
131
+ if receipts:
132
+ receipt = receipts[0]
133
+ logger.info(f"Posted new advertisement. Entity key: {receipt.entity_key}")
134
+ else:
135
+ logger.error("Failed to post advertisement: no receipt received")
@@ -0,0 +1,10 @@
1
+ from typing import List
2
+ from golem_base_sdk import GolemBaseClient, EntityKey, QueryEntitiesResult, GenericBytes
3
+ from ..config import settings
4
+
5
+ async def get_provider_entity_keys(client: GolemBaseClient, provider_id: str) -> List[EntityKey]:
6
+ """Get all entity keys for a given provider ID."""
7
+ query = f'golem_provider_id="{provider_id}"'
8
+ results: list[QueryEntitiesResult] = await client.query_entities(query)
9
+ # The entity_key from query_entities is a hex string, convert it to an EntityKey object
10
+ return [EntityKey(GenericBytes.from_hex_string(result.entity_key)) for result in results]
@@ -0,0 +1,34 @@
1
+ import psutil
2
+
3
+ class ResourceMonitor:
4
+ """Monitor system resources."""
5
+
6
+ @staticmethod
7
+ def get_cpu_count() -> int:
8
+ """Get number of CPU cores."""
9
+ return psutil.cpu_count()
10
+
11
+ @staticmethod
12
+ def get_memory_gb() -> int:
13
+ """Get available memory in GB."""
14
+ return psutil.virtual_memory().available // (1024 ** 3)
15
+
16
+ @staticmethod
17
+ def get_storage_gb() -> int:
18
+ """Get available storage in GB."""
19
+ return psutil.disk_usage("/").free // (1024 ** 3)
20
+
21
+ @staticmethod
22
+ def get_cpu_percent() -> float:
23
+ """Get CPU usage percentage."""
24
+ return psutil.cpu_percent(interval=1)
25
+
26
+ @staticmethod
27
+ def get_memory_percent() -> float:
28
+ """Get memory usage percentage."""
29
+ return psutil.virtual_memory().percent
30
+
31
+ @staticmethod
32
+ def get_storage_percent() -> float:
33
+ """Get storage usage percentage."""
34
+ return psutil.disk_usage("/").percent
@@ -11,7 +11,7 @@ class ResourceTracker:
11
11
 
12
12
  def __init__(self):
13
13
  """Initialize resource tracker."""
14
- from .advertiser import ResourceMonitor
14
+ from .resource_monitor import ResourceMonitor
15
15
  self.total_resources = {
16
16
  "cpu": ResourceMonitor.get_cpu_count(),
17
17
  "memory": ResourceMonitor.get_memory_gb(),
@@ -0,0 +1,24 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ from .advertiser import Advertiser
5
+ from ..config import settings
6
+
7
+ class AdvertisementService:
8
+ """Service for managing the advertisement lifecycle."""
9
+
10
+ def __init__(self, advertiser: Advertiser):
11
+ self.advertiser = advertiser
12
+ self._task: Optional[asyncio.Task] = None
13
+
14
+ async def start(self):
15
+ """Initialize and start the advertiser."""
16
+ await self.advertiser.initialize()
17
+ self._task = asyncio.create_task(self.advertiser.start_loop())
18
+
19
+ async def stop(self):
20
+ """Stop the advertiser."""
21
+ if self._task:
22
+ self._task.cancel()
23
+ await self._task
24
+ await self.advertiser.stop()