golem-vm-provider 0.1.24__py3-none-any.whl → 0.1.26__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,29 +1,29 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.24
3
+ Version: 0.1.26
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
7
7
  Author-email: phillip+vm-on-golem@golemgrid.com
8
- Requires-Python: >=3.9,<4.0
8
+ Requires-Python: >=3.13,<4.0
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Environment :: Console
11
11
  Classifier: Intended Audience :: System Administrators
12
12
  Classifier: Operating System :: POSIX :: Linux
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
14
  Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.9
19
16
  Classifier: Topic :: System :: Distributed Computing
20
17
  Requires-Dist: aiohttp (>=3.8.1,<4.0.0)
21
18
  Requires-Dist: colorlog (>=6.8.0,<7.0.0)
22
19
  Requires-Dist: cryptography (>=3.4.7,<4.0.0)
23
- Requires-Dist: eth-account (>=0.8.0,<0.9.0)
24
- Requires-Dist: fastapi (>=0.68.0,<0.69.0)
20
+ Requires-Dist: eth-account (>=0.13.6,<0.14.0)
21
+ Requires-Dist: fastapi (>=0.103.0,<0.104.0)
22
+ Requires-Dist: golem-base-sdk (==0.1.0)
23
+ Requires-Dist: httpx (>=0.23.0,<0.24.0)
25
24
  Requires-Dist: psutil (>=5.9.0,<6.0.0)
26
- Requires-Dist: pydantic (>=1.8.2,<2.0.0)
25
+ Requires-Dist: pydantic (>=2.4.0,<3.0.0)
26
+ Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
27
27
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
28
28
  Requires-Dist: python-jose[cryptography] (>=3.3.0,<4.0.0)
29
29
  Requires-Dist: python-multipart (>=0.0.5,<0.0.6)
@@ -31,7 +31,9 @@ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
31
31
  Requires-Dist: requests (>=2.31.0,<3.0.0)
32
32
  Requires-Dist: rich (>=13.7.0,<14.0.0)
33
33
  Requires-Dist: setuptools (>=69.0.3,<70.0.0)
34
+ Requires-Dist: typer (>=0.4.0,<0.5.0)
34
35
  Requires-Dist: uvicorn (>=0.15.0,<0.16.0)
36
+ Requires-Dist: web3 (==7.13.0)
35
37
  Project-URL: Homepage, https://github.com/cryptobench/vm-on-golem
36
38
  Project-URL: Repository, https://github.com/cryptobench/vm-on-golem
37
39
  Description-Content-Type: text/markdown
@@ -0,0 +1,30 @@
1
+ provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
2
+ provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
3
+ provider/api/models.py,sha256=9799o6ZnBfG2LZCoqm2-6ONNNgqKB7bn5oIs_Ocq24s,3519
4
+ provider/api/routes.py,sha256=P27RQvNqFWn6PacRwr1PaVz-yv5KAWsp9KeORejkXSI,6452
5
+ provider/config.py,sha256=pFW0Qwej9eTZfx1BXTExhsXwul-drqn0Lu8wPhx399Y,16908
6
+ provider/discovery/__init__.py,sha256=VR3NRoQtZRH5Vs8FG7jnGLR7p7wn7XeZdLaBb3t8e1g,123
7
+ provider/discovery/advertiser.py,sha256=yv7RbRf1K43qOLAEa2Olj9hhN8etl2qsBuoHok0xoVs,6784
8
+ provider/discovery/golem_base_advertiser.py,sha256=UpSJyO6wyujBJ2xDrb6lhKC7zYv1vZg0db4t0k--5dk,5830
9
+ provider/discovery/golem_base_utils.py,sha256=xk7vznhMgzrn0AuGyk6-9N9ukp9oPdBbbk1RI-sVjp0,607
10
+ provider/discovery/resource_tracker.py,sha256=8dYhJxoe_jLRwisHoA0jr575YhUKmLIqSXfW88KshcQ,6000
11
+ provider/main.py,sha256=7Fj9h13uVL3dPeukAc2U3q_HonMsRhTSnj86lj6Hqtc,9898
12
+ provider/network/port_verifier.py,sha256=3l6WNwBHydggJRFYkAsuBp1eCxaU619kjWuM-zSVj2o,13267
13
+ provider/security/ethereum.py,sha256=EwPZj4JR8OEpto6LhKjuuT3Z9pBX6P7-UQaqJtqFkYQ,1242
14
+ provider/security/faucet.py,sha256=O2DgP3bIrRUm9tdLCdgnda9em0rPyeW42sWhO1EQJaA,5363
15
+ provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
16
+ provider/utils/logging.py,sha256=C_elr0sJROHKQgErYpHJQvfujgh0k4Zf2gg8ZKfrmVk,2590
17
+ provider/utils/port_display.py,sha256=eLtkpYk5va1KWDQhEbxTYGymaXiPVpsYbvmdXM29IPo,12067
18
+ provider/utils/retry.py,sha256=ekP2ucaSJNN-lBcrIvyHa4QYPKNITMl1a5V1X6BBvsw,1560
19
+ provider/utils/setup.py,sha256=Z5dLuBQkb5vdoQsu1HJZwXmu9NWsiBYJ7Vq9-C-_tY8,2932
20
+ provider/vm/__init__.py,sha256=JGs50tUmzOR1rQ_w4fMY_3XWylmiA1G7KKWZkVw51mY,501
21
+ provider/vm/cloud_init.py,sha256=E5dDH7dqStRcJNDfbarBBe83-c9N63W8B5ycIrHI8eU,4627
22
+ provider/vm/models.py,sha256=RnfRQrziGBQZV_z6whQ8m328Abn2pItKZEZhT4S8AkU,6266
23
+ provider/vm/multipass.py,sha256=DOL_9fRXNu9LyWdW_HGCxG9C-iuuPnAF0u95sObb6Fk,16940
24
+ provider/vm/name_mapper.py,sha256=MrshNeJ4Dw-WBsyiIVcn9N5xyOxaBKX4Yqhyh_m5IFg,4103
25
+ provider/vm/port_manager.py,sha256=H7uLe-jJgBPtvnWrIHhRP-04sq11wMunOf0CFa_eDWE,11820
26
+ provider/vm/proxy_manager.py,sha256=aSzMVggH8BgE6L92yCSRtultEQiwHtPP8fnQzSF102Y,14941
27
+ golem_vm_provider-0.1.26.dist-info/METADATA,sha256=p4DYiuNE7QKUyIyev2sZVXiTMP3gshKOokTWGMepJ4M,10645
28
+ golem_vm_provider-0.1.26.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
29
+ golem_vm_provider-0.1.26.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
30
+ golem_vm_provider-0.1.26.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ golem-provider=provider.main:cli
3
+
provider/api/models.py CHANGED
@@ -11,11 +11,11 @@ logger = setup_logger(__name__)
11
11
  class CreateVMRequest(BaseModel):
12
12
  """Request model for creating a VM."""
13
13
  name: str = Field(..., min_length=3, max_length=64,
14
- regex="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
14
+ pattern="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
15
15
  size: Optional[VMSize] = None
16
16
  resources: Optional[VMResources] = None
17
17
  image: str = Field(default="24.04") # Ubuntu 24.04 LTS
18
- ssh_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ",
18
+ ssh_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ",
19
19
  description="SSH public key for VM access")
20
20
 
21
21
  @validator("name")
@@ -82,7 +82,7 @@ class VMResponse(BaseModel):
82
82
  class AddSSHKeyRequest(BaseModel):
83
83
  """Request model for adding SSH key."""
84
84
  name: str = Field(..., min_length=1, max_length=64)
85
- public_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ")
85
+ public_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ")
86
86
 
87
87
 
88
88
  class ErrorResponse(BaseModel):
provider/config.py CHANGED
@@ -3,7 +3,8 @@ from pathlib import Path
3
3
  from typing import Optional
4
4
  import uuid
5
5
 
6
- from pydantic import BaseSettings, validator, Field
6
+ from pydantic_settings import BaseSettings
7
+ from pydantic import validator, Field
7
8
  from .utils.logging import setup_logger
8
9
 
9
10
  logger = setup_logger(__name__)
@@ -16,13 +17,28 @@ class Settings(BaseSettings):
16
17
  DEBUG: bool = True
17
18
  HOST: str = "0.0.0.0"
18
19
  PORT: int = 7466
20
+ SKIP_PORT_VERIFICATION: bool = False
21
+ ENVIRONMENT: str = "production"
22
+
23
+ @property
24
+ def DEV_MODE(self) -> bool:
25
+ return self.ENVIRONMENT == "development"
26
+
27
+ @validator("SKIP_PORT_VERIFICATION", always=True)
28
+ def set_skip_verification(cls, v: bool, values: dict) -> bool:
29
+ """Set skip verification based on debug mode."""
30
+ return v or values.get("DEBUG", False)
19
31
 
20
32
  # Provider Settings
21
- PROVIDER_ID: str = "" # Will be set from Ethereum identity
22
33
  PROVIDER_NAME: str = "golem-provider"
23
34
  PROVIDER_COUNTRY: str = "SE"
24
35
  ETHEREUM_KEY_DIR: str = ""
25
-
36
+ ETHEREUM_PRIVATE_KEY: Optional[str] = None
37
+ PROVIDER_ID: str = "" # Will be set from Ethereum identity
38
+ FAUCET_URL: str = "https://ethwarsaw.holesky.golemdb.io/faucet"
39
+ CAPTCHA_URL: str = "https://cap.gobas.me"
40
+ CAPTCHA_API_KEY: str = "05381a2cef5e"
41
+
26
42
  @validator("ETHEREUM_KEY_DIR", pre=True)
27
43
  def resolve_key_dir(cls, v: str) -> str:
28
44
  """Resolve Ethereum key directory path."""
@@ -33,24 +49,56 @@ class Settings(BaseSettings):
33
49
  path = Path.home() / path
34
50
  return str(path)
35
51
 
36
- @validator("PROVIDER_ID", always=True)
37
- def get_or_create_provider_id(cls, v: str, values: dict) -> str:
38
- """Get or create provider ID from Ethereum identity."""
52
+ @validator("ETHEREUM_PRIVATE_KEY", always=True)
53
+ def get_private_key(cls, v: Optional[str], values: dict) -> str:
54
+ """Get private key from key file if not provided."""
39
55
  from provider.security.ethereum import EthereumIdentity
40
56
 
41
- # If ID provided in env, use it
42
57
  if v:
43
58
  return v
44
-
45
- # Get ID from Ethereum identity
59
+
46
60
  key_dir = values.get("ETHEREUM_KEY_DIR")
47
61
  identity = EthereumIdentity(key_dir)
48
- return identity.get_or_create_identity()
62
+ _, private_key = identity.get_or_create_identity()
63
+ return private_key
49
64
 
65
+ @validator("PROVIDER_ID", always=True)
66
+ def get_provider_id(cls, v: str, values: dict) -> str:
67
+ """Get provider ID from private key."""
68
+ from eth_account import Account
69
+
70
+ private_key = values.get("ETHEREUM_PRIVATE_KEY")
71
+ if not private_key:
72
+ raise ValueError("ETHEREUM_PRIVATE_KEY is not set")
73
+
74
+ acct = Account.from_key(private_key)
75
+ provider_id_from_key = acct.address
76
+
77
+ # If ID was provided via env, warn if it doesn't match
78
+ if v and v != provider_id_from_key:
79
+ logger.warning(
80
+ f"Provider ID from env ('{v}') does not match ID from key file ('{provider_id_from_key}'). "
81
+ "Using ID from key file."
82
+ )
83
+
84
+ return provider_id_from_key
85
+
86
+ @validator("PROVIDER_NAME", always=True)
87
+ def set_provider_name(cls, v: str, values: dict) -> str:
88
+ """Prefix provider name with DEVMODE if in development."""
89
+ if values.get("ENVIRONMENT") == "development":
90
+ return f"DEVMODE-{v}"
91
+ return v
92
+
50
93
  # Discovery Service Settings
51
94
  DISCOVERY_URL: str = "http://195.201.39.101:9001"
95
+ DISCOVERY_DRIVER: str = "golem-base" # or "legacy"
52
96
  ADVERTISEMENT_INTERVAL: int = 240 # seconds
53
97
 
98
+ # Golem Base Settings
99
+ GOLEM_BASE_RPC_URL: str = "https://ethwarsaw.holesky.golemdb.io/rpc"
100
+ GOLEM_BASE_WS_URL: str = "wss://ethwarsaw.holesky.golemdb.io/rpc/ws"
101
+
54
102
  # VM Settings
55
103
  MAX_VMS: int = 10
56
104
  DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
@@ -345,9 +393,19 @@ class Settings(BaseSettings):
345
393
  return str(path)
346
394
 
347
395
  @validator("PUBLIC_IP", pre=True)
348
- def get_public_ip(cls, v: Optional[str]) -> Optional[str]:
396
+ def get_public_ip(cls, v: Optional[str], values: dict) -> Optional[str]:
349
397
  """Get public IP if set to 'auto'."""
350
398
  if v == "auto":
399
+ if values.get("ENVIRONMENT") == "development":
400
+ import socket
401
+ try:
402
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
403
+ s.connect(("8.8.8.8", 80))
404
+ local_ip = s.getsockname()[0]
405
+ s.close()
406
+ return local_ip
407
+ except Exception:
408
+ return "127.0.0.1"
351
409
  try:
352
410
  import requests
353
411
  response = requests.get("https://api.ipify.org")
@@ -0,0 +1,129 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ from golem_base_sdk import GolemBaseClient, GolemBaseCreate, GolemBaseUpdate, GolemBaseDelete, Annotation
5
+ from .golem_base_utils import get_provider_entity_keys
6
+ from ..config import settings
7
+ from ..utils.logging import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+
12
+ class GolemBaseAdvertiser:
13
+ """Advertise available resources to the Golem Base network."""
14
+
15
+ def __init__(self, resource_tracker: "ResourceTracker"):
16
+ self.resource_tracker = resource_tracker
17
+ self.client: Optional[GolemBaseClient] = None
18
+ self._stop_event = asyncio.Event()
19
+
20
+ async def initialize(self):
21
+ """Initialize the advertiser."""
22
+ private_key_hex = settings.ETHEREUM_PRIVATE_KEY.replace("0x", "")
23
+ private_key_bytes = bytes.fromhex(private_key_hex)
24
+ self.client = await GolemBaseClient.create(
25
+ rpc_url=settings.GOLEM_BASE_RPC_URL,
26
+ ws_url=settings.GOLEM_BASE_WS_URL,
27
+ private_key=private_key_bytes,
28
+ )
29
+ self.resource_tracker.on_update(
30
+ lambda: asyncio.create_task(self.post_advertisement())
31
+ )
32
+
33
+ async def start_loop(self):
34
+ """Start advertising resources in a loop."""
35
+ try:
36
+ while not self._stop_event.is_set():
37
+ await self.post_advertisement()
38
+ await asyncio.sleep(settings.ADVERTISEMENT_INTERVAL)
39
+ finally:
40
+ await self.stop()
41
+
42
+ async def stop(self):
43
+ """Stop advertising resources."""
44
+ self._stop_event.set()
45
+ if self.client:
46
+ await self.client.disconnect()
47
+
48
+ async def post_advertisement(self):
49
+ """Post or update resource advertisement on the Golem Base network."""
50
+ if not self.client:
51
+ raise RuntimeError("Golem Base client not initialized")
52
+
53
+ resources = self.resource_tracker.get_available_resources()
54
+ if not self.resource_tracker._meets_minimum_requirements(resources):
55
+ logger.warning("Resources too low, skipping advertisement")
56
+ return
57
+
58
+ ip_address = settings.PUBLIC_IP
59
+ if not ip_address:
60
+ logger.error("Could not get public IP, skipping advertisement")
61
+ return
62
+
63
+ try:
64
+ existing_keys = await get_provider_entity_keys(self.client, settings.PROVIDER_ID)
65
+
66
+ string_annotations = [
67
+ Annotation(key="golem_type", value="provider"),
68
+ Annotation(key="golem_provider_id", value=settings.PROVIDER_ID),
69
+ Annotation(key="golem_ip_address", value=ip_address),
70
+ Annotation(key="golem_country", value=settings.PROVIDER_COUNTRY),
71
+ Annotation(key="golem_provider_name", value=settings.PROVIDER_NAME),
72
+ ]
73
+ numeric_annotations = [
74
+ Annotation(key="golem_cpu", value=resources["cpu"]),
75
+ Annotation(key="golem_memory", value=resources["memory"]),
76
+ Annotation(key="golem_storage", value=resources["storage"]),
77
+ ]
78
+
79
+ if len(existing_keys) > 1:
80
+ logger.warning(f"Found {len(existing_keys)} advertisements. Cleaning up and creating a new one.")
81
+ deletes = [GolemBaseDelete(entity_key=key) for key in existing_keys]
82
+ await self.client.delete_entities(deletes)
83
+ await self._create_advertisement(string_annotations, numeric_annotations)
84
+
85
+ elif len(existing_keys) == 1:
86
+ entity_key = existing_keys[0]
87
+ metadata = await self.client.get_entity_metadata(entity_key)
88
+
89
+ current_annotations = {ann.key: ann.value for ann in metadata.numeric_annotations}
90
+ current_annotations.update({ann.key: ann.value for ann in metadata.string_annotations})
91
+
92
+ if (current_annotations.get("golem_cpu") == resources["cpu"] and
93
+ current_annotations.get("golem_memory") == resources["memory"] and
94
+ current_annotations.get("golem_storage") == resources["storage"] and
95
+ current_annotations.get("golem_ip_address") == ip_address and
96
+ current_annotations.get("golem_provider_name") == settings.PROVIDER_NAME):
97
+ logger.info("Advertisement is up-to-date. Waiting for expiration.")
98
+ else:
99
+ logger.info("Advertisement is outdated. Updating.")
100
+ update = GolemBaseUpdate(
101
+ entity_key=entity_key,
102
+ data=b"",
103
+ btl=settings.ADVERTISEMENT_INTERVAL * 2,
104
+ string_annotations=string_annotations,
105
+ numeric_annotations=numeric_annotations,
106
+ )
107
+ await self.client.update_entities([update])
108
+ logger.info(f"Updated advertisement. Entity key: {entity_key}")
109
+
110
+ else: # No existing keys
111
+ await self._create_advertisement(string_annotations, numeric_annotations)
112
+
113
+ except Exception as e:
114
+ logger.error(f"Failed to post or update advertisement on Golem Base: {e}")
115
+
116
+ async def _create_advertisement(self, string_annotations, numeric_annotations):
117
+ """Helper to create a new advertisement."""
118
+ entity = GolemBaseCreate(
119
+ data=b"",
120
+ btl=settings.ADVERTISEMENT_INTERVAL * 2,
121
+ string_annotations=string_annotations,
122
+ numeric_annotations=numeric_annotations,
123
+ )
124
+ receipts = await self.client.create_entities([entity])
125
+ if receipts:
126
+ receipt = receipts[0]
127
+ logger.info(f"Posted new advertisement. Entity key: {receipt.entity_key}")
128
+ else:
129
+ 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]
provider/main.py CHANGED
@@ -1,6 +1,8 @@
1
+ from .api import routes
1
2
  import asyncio
2
3
  import os
3
4
  from fastapi import FastAPI
5
+ from fastapi.middleware.cors import CORSMiddleware
4
6
  from typing import Optional
5
7
 
6
8
  from .config import settings
@@ -8,12 +10,23 @@ from .utils.logging import setup_logger, PROCESS, SUCCESS
8
10
  from .utils.ascii_art import startup_animation
9
11
  from .discovery.resource_tracker import ResourceTracker
10
12
  from .discovery.advertiser import ResourceAdvertiser
13
+ from .discovery.golem_base_advertiser import GolemBaseAdvertiser
11
14
  from .vm.multipass import MultipassProvider
12
15
  from .vm.port_manager import PortManager
16
+ from .security.faucet import FaucetClient
13
17
 
14
18
  logger = setup_logger(__name__)
15
19
 
16
20
  app = FastAPI(title="VM on Golem Provider")
21
+ # Add CORS middleware
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"], # Allows all origins
25
+ allow_credentials=True,
26
+ allow_methods=["*"], # Allows all methods
27
+ allow_headers=["*"], # Allows all headers
28
+ )
29
+
17
30
 
18
31
  async def setup_provider() -> None:
19
32
  """Setup and initialize the provider components."""
@@ -25,66 +38,73 @@ async def setup_provider() -> None:
25
38
 
26
39
  # Create provider with resource tracker and temporary port manager
27
40
  logger.process("🔄 Initializing VM provider...")
28
- provider = MultipassProvider(resource_tracker, port_manager=None) # Will be set later
29
-
41
+ provider = MultipassProvider(
42
+ resource_tracker, port_manager=None) # Will be set later
43
+
30
44
  try:
31
45
  # Initialize provider (without port operations)
32
46
  await asyncio.wait_for(provider.initialize(), timeout=30)
33
-
47
+
34
48
  # Store provider reference
35
49
  app.state.provider = provider
36
50
  app.state.proxy_manager = provider.proxy_manager
37
-
51
+
38
52
  # Initialize port manager first to verify all ports
39
53
  logger.process("🔄 Initializing port manager...")
40
54
  port_manager = PortManager(
41
55
  start_port=settings.PORT_RANGE_START,
42
56
  end_port=settings.PORT_RANGE_END,
43
- discovery_port=settings.PORT
57
+ discovery_port=settings.PORT,
58
+ skip_verification=settings.SKIP_PORT_VERIFICATION
44
59
  )
45
-
60
+
46
61
  if not await port_manager.initialize():
47
62
  raise RuntimeError("Port verification failed")
48
-
63
+
49
64
  # Store port manager references
50
65
  app.state.port_manager = port_manager
51
66
  provider.port_manager = port_manager
52
67
  app.state.proxy_manager.port_manager = port_manager
53
-
68
+
54
69
  # Now restore proxy configurations using only verified ports
55
70
  logger.process("🔄 Restoring proxy configurations...")
56
71
  await app.state.proxy_manager._load_state()
57
-
72
+
58
73
  except asyncio.TimeoutError:
59
74
  logger.error("Provider initialization timed out")
60
75
  raise
61
76
  except Exception as e:
62
77
  logger.error(f"Failed to initialize provider: {e}")
63
78
  raise
64
-
65
- # Create and start advertiser in background
66
- logger.process("🔄 Starting resource advertiser...")
67
- advertiser = ResourceAdvertiser(
68
- resource_tracker=resource_tracker,
69
- discovery_url=settings.DISCOVERY_URL,
70
- provider_id=settings.PROVIDER_ID
71
- )
72
-
73
- # Start advertiser in background task
74
- app.state.advertiser_task = asyncio.create_task(advertiser.start())
79
+
80
+ # Create advertiser
81
+ logger.process("🔄 Initializing resource advertiser...")
82
+ if settings.DISCOVERY_DRIVER == "golem-base":
83
+ advertiser = GolemBaseAdvertiser(
84
+ resource_tracker=resource_tracker
85
+ )
86
+ await advertiser.initialize()
87
+ else:
88
+ advertiser = ResourceAdvertiser(
89
+ resource_tracker=resource_tracker,
90
+ discovery_url=settings.DISCOVERY_URL,
91
+ provider_id=settings.PROVIDER_ID
92
+ )
75
93
  app.state.advertiser = advertiser
76
-
77
- logger.success("✨ Provider setup complete and ready to accept requests")
94
+
95
+ logger.success(
96
+ "✨ Provider setup complete and ready to accept requests")
78
97
  except Exception as e:
79
98
  logger.error(f"Failed to setup provider: {e}")
80
99
  # Attempt cleanup of any initialized components
81
100
  await cleanup_provider()
82
101
  raise
83
102
 
103
+
84
104
  async def cleanup_provider() -> None:
85
105
  """Cleanup provider components."""
86
106
  cleanup_errors = []
87
-
107
+
88
108
  # Stop advertiser
89
109
  if hasattr(app.state, "advertiser"):
90
110
  try:
@@ -97,7 +117,7 @@ async def cleanup_provider() -> None:
97
117
  pass
98
118
  except Exception as e:
99
119
  cleanup_errors.append(f"Failed to stop advertiser: {e}")
100
-
120
+
101
121
  # Cleanup proxy manager first to stop all proxy servers
102
122
  if hasattr(app.state, "proxy_manager"):
103
123
  try:
@@ -106,7 +126,7 @@ async def cleanup_provider() -> None:
106
126
  cleanup_errors.append("Proxy manager cleanup timed out")
107
127
  except Exception as e:
108
128
  cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
109
-
129
+
110
130
  # Cleanup provider
111
131
  if hasattr(app.state, "provider"):
112
132
  try:
@@ -115,71 +135,56 @@ async def cleanup_provider() -> None:
115
135
  cleanup_errors.append("Provider cleanup timed out")
116
136
  except Exception as e:
117
137
  cleanup_errors.append(f"Failed to cleanup provider: {e}")
118
-
138
+
119
139
  if cleanup_errors:
120
140
  error_msg = "\n".join(cleanup_errors)
121
141
  logger.error(f"Errors during cleanup:\n{error_msg}")
122
142
  else:
123
143
  logger.success("✨ Provider cleanup complete")
124
144
 
145
+
125
146
  @app.on_event("startup")
126
147
  async def startup_event():
127
148
  """Handle application startup."""
128
149
  try:
129
150
  # Display startup animation
130
151
  await startup_animation()
152
+
153
+ # Initialize provider
154
+ await setup_provider()
131
155
 
132
- # Verify ports first
133
- from .vm.port_manager import PortManager
134
- from .utils.port_display import PortVerificationDisplay
135
- from .config import settings
136
-
137
- display = PortVerificationDisplay(
138
- provider_port=settings.PORT,
139
- port_range_start=settings.PORT_RANGE_START,
140
- port_range_end=settings.PORT_RANGE_END
141
- )
142
- display.print_header()
143
-
144
- # Initialize port manager
145
- logger.process("🔄 Verifying port accessibility...")
146
- port_manager = PortManager(
147
- start_port=settings.PORT_RANGE_START,
148
- end_port=settings.PORT_RANGE_END,
149
- discovery_port=settings.PORT
156
+ # Check wallet balance and request funds if needed
157
+ faucet_client = FaucetClient(
158
+ faucet_url=settings.FAUCET_URL,
159
+ captcha_url=settings.CAPTCHA_URL,
160
+ captcha_api_key=settings.CAPTCHA_API_KEY,
150
161
  )
151
- if not await port_manager.initialize():
152
- logger.error("Port verification failed. Please ensure:")
153
- logger.error(f"1. Port {settings.PORT} is accessible for provider access")
154
- logger.error(f"2. Some ports in range {settings.PORT_RANGE_START}-{settings.PORT_RANGE_END} are accessible for VM access")
155
- logger.error("3. Your firewall/router is properly configured")
156
- raise RuntimeError("Port verification failed")
157
-
158
- logger.success(f"✅ Port verification successful - {len(port_manager.verified_ports)} ports available")
159
-
160
- # Store port manager in app state for later use
161
- app.state.port_manager = port_manager
162
+ await faucet_client.get_funds(settings.PROVIDER_ID)
163
+
164
+ # Post initial advertisement and start advertising loop
165
+ if isinstance(app.state.advertiser, GolemBaseAdvertiser):
166
+ await app.state.advertiser.post_advertisement()
167
+ app.state.advertiser_task = asyncio.create_task(app.state.advertiser.start_loop())
162
168
 
163
- # Initialize provider
164
- await setup_provider()
165
169
  except Exception as e:
166
170
  logger.error(f"Startup failed: {e}")
167
171
  # Ensure proper cleanup
168
172
  await cleanup_provider()
169
173
  raise
170
174
 
175
+
171
176
  @app.on_event("shutdown")
172
177
  async def shutdown_event():
173
178
  """Handle application shutdown."""
174
179
  await cleanup_provider()
175
180
 
176
181
  # Import routes after app creation to avoid circular imports
177
- from .api import routes
178
182
  app.include_router(routes.router, prefix="/api/v1")
179
183
 
180
184
  # Export app for uvicorn
181
185
  __all__ = ["app", "start"]
182
186
 
187
+
183
188
  def check_requirements():
184
189
  """Check if all requirements are met."""
185
190
  try:
@@ -190,12 +195,13 @@ def check_requirements():
190
195
  logger.error(f"Requirements check failed: {e}")
191
196
  return False
192
197
 
198
+
193
199
  async def verify_provider_port(port: int) -> bool:
194
200
  """Verify that the provider port is available for binding.
195
-
201
+
196
202
  Args:
197
203
  port: The port to verify
198
-
204
+
199
205
  Returns:
200
206
  bool: True if the port is available, False otherwise
201
207
  """
@@ -218,7 +224,13 @@ async def verify_provider_port(port: int) -> bool:
218
224
  logger.error("3. Your firewall allows binding to this port")
219
225
  return False
220
226
 
221
- def start():
227
+
228
+ import typer
229
+
230
+ cli = typer.Typer()
231
+
232
+ @cli.command()
233
+ def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification.")):
222
234
  """Start the provider server."""
223
235
  import sys
224
236
  from pathlib import Path
@@ -226,15 +238,15 @@ def start():
226
238
  import uvicorn
227
239
  from .utils.logging import setup_logger
228
240
  from .config import settings
229
-
241
+
230
242
  # Configure logging with debug mode
231
243
  logger = setup_logger(__name__, debug=True)
232
-
244
+
233
245
  try:
234
246
  # Load environment variables from .env file
235
247
  env_path = Path(__file__).parent.parent / '.env'
236
248
  load_dotenv(dotenv_path=env_path)
237
-
249
+
238
250
  # Log environment variables
239
251
  logger.info("Environment variables:")
240
252
  for key, value in os.environ.items():
@@ -245,18 +257,19 @@ def start():
245
257
  if not check_requirements():
246
258
  logger.error("Requirements check failed")
247
259
  sys.exit(1)
248
-
260
+
249
261
  # Verify provider port is available
250
- if not asyncio.run(verify_provider_port(settings.PORT)):
262
+ if not no_verify_port and not asyncio.run(verify_provider_port(settings.PORT)):
251
263
  logger.error(f"Provider port {settings.PORT} is not available")
252
264
  sys.exit(1)
253
-
265
+
254
266
  # Configure uvicorn logging
255
267
  log_config = uvicorn.config.LOGGING_CONFIG
256
268
  log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
257
-
269
+
258
270
  # Run server
259
- logger.process(f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
271
+ logger.process(
272
+ f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
260
273
  uvicorn.run(
261
274
  "provider:app",
262
275
  host=settings.HOST,
@@ -270,3 +283,6 @@ def start():
270
283
  except Exception as e:
271
284
  logger.error(f"Failed to start provider server: {e}")
272
285
  sys.exit(1)
286
+
287
+ if __name__ == "__main__":
288
+ cli()
@@ -13,29 +13,25 @@ class EthereumIdentity:
13
13
  self.key_dir = Path(key_dir)
14
14
  self.key_file = self.key_dir / "provider_key.json"
15
15
 
16
- def get_or_create_identity(self) -> str:
17
- """Get existing provider ID or create new one from Ethereum key."""
18
- # Create directory if it doesn't exist
16
+ def get_or_create_identity(self) -> (str, str):
17
+ """Get existing provider ID and private key, or create a new one."""
19
18
  self.key_dir.mkdir(parents=True, exist_ok=True)
20
- self.key_dir.chmod(0o700) # Secure permissions
21
-
22
- # Check for existing key
19
+ self.key_dir.chmod(0o700)
20
+
23
21
  if self.key_file.exists():
24
- with open(self.key_file) as f:
22
+ with open(self.key_file, "r") as f:
25
23
  key_data = json.load(f)
26
- return key_data["address"]
27
-
28
- # Generate new key
24
+ return key_data["address"], key_data["private_key"]
25
+
29
26
  Account.enable_unaudited_hdwallet_features()
30
27
  acct = Account.create()
31
28
 
32
- # Save key securely
33
29
  key_data = {
34
30
  "address": acct.address,
35
31
  "private_key": acct.key.hex()
36
32
  }
37
33
  with open(self.key_file, "w") as f:
38
34
  json.dump(key_data, f)
39
- self.key_file.chmod(0o600) # Secure permissions
35
+ self.key_file.chmod(0o600)
40
36
 
41
- return acct.address
37
+ return acct.address, acct.key.hex()
@@ -0,0 +1,132 @@
1
+ import asyncio
2
+ import hashlib
3
+ import httpx
4
+ from typing import Optional
5
+
6
+ from golem_base_sdk import GolemBaseClient
7
+ from provider.utils.logging import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+
12
+ class FaucetClient:
13
+ """A client for interacting with a Proof of Work-protected faucet."""
14
+
15
+ def __init__(self, faucet_url: str, captcha_url: str, captcha_api_key: str):
16
+ self.faucet_url = faucet_url
17
+ self.captcha_url = captcha_url
18
+ self.captcha_api_key = captcha_api_key
19
+ self.api_endpoint = f"{faucet_url}/api"
20
+ self.client: Optional[GolemBaseClient] = None
21
+
22
+ async def _ensure_client(self):
23
+ if not self.client:
24
+ from ..config import settings
25
+ private_key_hex = settings.ETHEREUM_PRIVATE_KEY.replace("0x", "")
26
+ private_key_bytes = bytes.fromhex(private_key_hex)
27
+ self.client = await GolemBaseClient.create_ro_client(
28
+ rpc_url=settings.GOLEM_BASE_RPC_URL,
29
+ ws_url=settings.GOLEM_BASE_WS_URL,
30
+ )
31
+
32
+ async def check_balance(self, address: str) -> Optional[float]:
33
+ """Check the balance of the given address."""
34
+ await self._ensure_client()
35
+ try:
36
+ balance_wei = await self.client.http_client().eth.get_balance(address)
37
+ balance_eth = self.client.http_client().from_wei(balance_wei, 'ether')
38
+ return float(balance_eth)
39
+ except Exception as e:
40
+ logger.error(f"Failed to check balance: {e}")
41
+ return None
42
+
43
+ async def get_funds(self, address: str) -> Optional[str]:
44
+ """Request funds from the faucet for the given address."""
45
+ try:
46
+ balance = await self.check_balance(address)
47
+ if balance is not None and balance > 0.01:
48
+ logger.info(f"Sufficient funds ({balance} ETH), skipping faucet request.")
49
+ return None
50
+
51
+ logger.info("Requesting funds from faucet...")
52
+ challenge_data = await self._get_challenge()
53
+ if not challenge_data:
54
+ return None
55
+
56
+ challenge_list = challenge_data.get("challenge")
57
+ token = challenge_data.get("token")
58
+
59
+ if not challenge_list or not token:
60
+ logger.error(f"Invalid challenge data received: {challenge_data}")
61
+ return None
62
+
63
+ solutions = []
64
+ for salt, target in challenge_list:
65
+ nonce = self._solve_challenge(salt, target)
66
+ solutions.append([salt, target, nonce])
67
+
68
+ redeemed_token = await self._redeem_solution(token, solutions)
69
+ if not redeemed_token:
70
+ return None
71
+
72
+ tx_hash = await self._request_faucet(address, redeemed_token)
73
+ if tx_hash:
74
+ logger.success(f"Successfully requested funds. Transaction hash: {tx_hash}")
75
+ return tx_hash
76
+ except Exception as e:
77
+ import traceback
78
+ logger.error(f"Failed to get funds from faucet: {e}")
79
+ logger.error(traceback.format_exc())
80
+ return None
81
+
82
+ async def _get_challenge(self) -> Optional[dict]:
83
+ """Get a PoW challenge from the faucet."""
84
+ try:
85
+ async with httpx.AsyncClient(timeout=60.0) as client:
86
+ url = f"{self.captcha_url}/{self.captcha_api_key}/api/challenge"
87
+ response = await client.post(url)
88
+ response.raise_for_status()
89
+ return response.json()
90
+ except httpx.HTTPStatusError as e:
91
+ logger.error(f"Failed to get PoW challenge: {e.response.text}")
92
+ return None
93
+
94
+ def _solve_challenge(self, salt: str, target: str) -> int:
95
+ """Solve the PoW challenge."""
96
+ target_hash = bytes.fromhex(target)
97
+ nonce = 0
98
+ while True:
99
+ hasher = hashlib.sha256()
100
+ hasher.update(f"{salt}{nonce}".encode())
101
+ if hasher.digest().startswith(target_hash):
102
+ return nonce
103
+ nonce += 1
104
+
105
+ async def _redeem_solution(self, token: str, solutions: list) -> Optional[str]:
106
+ """Redeem the PoW solution to get a CAPTCHA token."""
107
+ try:
108
+ async with httpx.AsyncClient(timeout=60.0) as client:
109
+ url = f"{self.captcha_url}/{self.captcha_api_key}/api/redeem"
110
+ response = await client.post(
111
+ url,
112
+ json={"token": token, "solutions": solutions}
113
+ )
114
+ response.raise_for_status()
115
+ return response.json().get("token")
116
+ except httpx.HTTPStatusError as e:
117
+ logger.error(f"Failed to redeem PoW solution: {e.response.text}")
118
+ return None
119
+
120
+ async def _request_faucet(self, address: str, token: str) -> Optional[str]:
121
+ """Request funds from the faucet with the CAPTCHA token."""
122
+ try:
123
+ async with httpx.AsyncClient(timeout=60.0) as client:
124
+ response = await client.post(
125
+ f"{self.api_endpoint}/faucet",
126
+ json={"address": address, "captchaToken": token}
127
+ )
128
+ response.raise_for_status()
129
+ return response.json().get("txHash")
130
+ except httpx.HTTPStatusError as e:
131
+ logger.error(f"Faucet request failed: {e.response.text}")
132
+ return None
@@ -10,17 +10,25 @@ class PortVerificationDisplay:
10
10
 
11
11
  SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
12
12
 
13
- def __init__(self, provider_port: int, port_range_start: int, port_range_end: int):
13
+ def __init__(
14
+ self,
15
+ provider_port: int,
16
+ port_range_start: int,
17
+ port_range_end: int,
18
+ skip_verification: bool = False
19
+ ):
14
20
  """Initialize the display.
15
21
 
16
22
  Args:
17
23
  provider_port: Port used for provider access
18
24
  port_range_start: Start of VM access port range
19
25
  port_range_end: End of VM access port range
26
+ skip_verification: Whether port verification is skipped (development mode)
20
27
  """
21
28
  self.provider_port = provider_port
22
29
  self.port_range_start = port_range_start
23
30
  self.port_range_end = port_range_end
31
+ self.skip_verification = skip_verification
24
32
  self.spinner_idx = 0
25
33
 
26
34
  def _update_spinner(self):
@@ -91,6 +99,13 @@ class PortVerificationDisplay:
91
99
  print("\n🔒 VM Access Ports (Required)")
92
100
  print("-------------------------")
93
101
 
102
+ if self.skip_verification:
103
+ print("\n✅ All ports available in development mode")
104
+ print(f"└─ Port Range: {self.port_range_start}-{self.port_range_end}")
105
+ print("└─ Status: Port verification skipped")
106
+ print("└─ Note: Configure ports before deploying to production")
107
+ return
108
+
94
109
  await self.animate_verification("Scanning VM access ports...")
95
110
 
96
111
  # Calculate progress
@@ -125,6 +140,9 @@ class PortVerificationDisplay:
125
140
  discovery_result: Verification result for discovery port
126
141
  ssh_results: Dictionary mapping SSH ports to their verification results
127
142
  """
143
+ if self.skip_verification:
144
+ return
145
+
128
146
  issues = []
129
147
 
130
148
  # Check discovery port
@@ -155,6 +173,9 @@ class PortVerificationDisplay:
155
173
  discovery_result: Verification result for discovery port
156
174
  ssh_results: Dictionary mapping SSH ports to their verification results
157
175
  """
176
+ if self.skip_verification:
177
+ return
178
+
158
179
  # Check if we have any issues
159
180
  has_issues = (
160
181
  not discovery_result.accessible or
@@ -186,6 +207,13 @@ class PortVerificationDisplay:
186
207
  ssh_results: Dictionary mapping SSH ports to their verification results
187
208
  """
188
209
  print("\n🎯 Current Status:", end=" ")
210
+
211
+ if self.skip_verification:
212
+ print("Development Mode")
213
+ print("└─ Status: Port verification skipped")
214
+ print(f"└─ Available: All ports in range {self.port_range_start}-{self.port_range_end}")
215
+ print("└─ Note: This is for development only, configure ports in production")
216
+ return
189
217
 
190
218
  if discovery_result is None:
191
219
  print("Verification Failed")
provider/vm/models.py CHANGED
@@ -57,13 +57,13 @@ class VMResources(BaseModel):
57
57
  class VMCreateRequest(BaseModel):
58
58
  """Request to create a new VM."""
59
59
  name: str = Field(..., min_length=3, max_length=64,
60
- regex="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
60
+ pattern="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
61
61
  size: Optional[VMSize] = None
62
62
  cpu_cores: Optional[int] = None
63
63
  memory_gb: Optional[int] = None
64
64
  storage_gb: Optional[int] = None
65
65
  image: Optional[str] = Field(default="24.04") # Ubuntu 24.04 LTS
66
- ssh_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ",
66
+ ssh_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ",
67
67
  description="SSH public key for VM access")
68
68
 
69
69
  @validator("name")
@@ -91,11 +91,11 @@ class VMCreateRequest(BaseModel):
91
91
  class VMConfig(BaseModel):
92
92
  """VM configuration."""
93
93
  name: str = Field(..., min_length=3, max_length=64,
94
- regex="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
94
+ pattern="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
95
95
  resources: VMResources
96
96
  image: str = Field(default="24.04") # Ubuntu 24.04 LTS
97
97
  size: Optional[VMSize] = None
98
- ssh_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ",
98
+ ssh_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ",
99
99
  description="SSH public key for VM access")
100
100
 
101
101
  @validator("name")
@@ -127,7 +127,7 @@ class VMInfo(BaseModel):
127
127
  class SSHKey(BaseModel):
128
128
  """SSH key information."""
129
129
  name: str = Field(..., min_length=1, max_length=64)
130
- public_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ")
130
+ public_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ")
131
131
  fingerprint: Optional[str] = None
132
132
 
133
133
 
provider/vm/multipass.py CHANGED
@@ -198,7 +198,11 @@ class MultipassProvider(VMProvider):
198
198
  # First allocate a verified port
199
199
  ssh_port = self.port_manager.allocate_port(multipass_name)
200
200
  if not ssh_port:
201
- raise MultipassError("Failed to allocate verified SSH port")
201
+ if settings.DEV_MODE:
202
+ logger.warning("Failed to allocate verified SSH port in dev mode, falling back to random port")
203
+ ssh_port = 0 # Let the proxy manager pick a random port
204
+ else:
205
+ raise MultipassError("Failed to allocate verified SSH port")
202
206
 
203
207
  # Then configure proxy with allocated port
204
208
  success = await self.proxy_manager.add_vm(multipass_name, ip_address, port=ssh_port)
@@ -24,7 +24,8 @@ class PortManager:
24
24
  state_file: Optional[str] = None,
25
25
  port_check_servers: Optional[List[str]] = None,
26
26
  discovery_port: Optional[int] = None,
27
- existing_ports: Optional[Set[int]] = None
27
+ existing_ports: Optional[Set[int]] = None,
28
+ skip_verification: bool = False
28
29
  ):
29
30
  """Initialize the port manager.
30
31
 
@@ -51,6 +52,7 @@ class PortManager:
51
52
  "http://195.201.39.101:9000", # Production servers
52
53
  ]
53
54
  self.discovery_port = discovery_port or settings.PORT
55
+ self.skip_verification = skip_verification
54
56
  self.port_verifier = PortVerifier(
55
57
  self.port_check_servers,
56
58
  discovery_port=self.discovery_port
@@ -76,20 +78,34 @@ class PortManager:
76
78
  display = PortVerificationDisplay(
77
79
  provider_port=self.discovery_port,
78
80
  port_range_start=self.start_port,
79
- port_range_end=self.end_port
81
+ port_range_end=self.end_port,
82
+ skip_verification=self.skip_verification
80
83
  )
81
84
  display.print_header()
82
85
 
83
- # Verify all ports in range, including existing ones
84
- ssh_ports = list(range(self.start_port, self.end_port))
85
- logger.info(f"Starting port verification...")
86
- logger.info(f"SSH ports range: {self.start_port}-{self.end_port}")
87
- logger.info(
88
- f"Using port check servers: {', '.join(self.port_check_servers)}")
89
-
90
- # Clear existing verified ports before verification
91
- self.verified_ports.clear()
92
- results = await self.port_verifier.verify_ports(ssh_ports)
86
+ # If verification is skipped, mark all ports as verified
87
+ if self.skip_verification:
88
+ logger.warning("⚠️ Port verification is disabled in development mode")
89
+ logger.warning(" All ports will be considered available")
90
+ logger.warning(" This should only be used for development/testing")
91
+
92
+ # Mark all ports as verified
93
+ self.verified_ports = set(range(self.start_port, self.end_port))
94
+
95
+ # In development mode, we don't need to create any results
96
+ # The display will handle development mode differently
97
+ results = {}
98
+ else:
99
+ # Verify all ports in range, including existing ones
100
+ ssh_ports = list(range(self.start_port, self.end_port))
101
+ logger.info(f"Starting port verification...")
102
+ logger.info(f"SSH ports range: {self.start_port}-{self.end_port}")
103
+ logger.info(
104
+ f"Using port check servers: {', '.join(self.port_check_servers)}")
105
+
106
+ # Clear existing verified ports before verification
107
+ self.verified_ports.clear()
108
+ results = await self.port_verifier.verify_ports(ssh_ports)
93
109
 
94
110
  # Add provider port as verified since we already checked it
95
111
  results[self.discovery_port] = PortVerificationResult(
@@ -141,13 +157,17 @@ class PortManager:
141
157
  # Print precise summary of current status
142
158
  display.print_summary(discovery_result, ssh_results)
143
159
 
144
- if not self.verified_ports:
145
- logger.error("No SSH ports were verified as accessible")
146
- return False
160
+ if self.skip_verification:
161
+ logger.info(f"Port verification skipped - all {len(self.verified_ports)} ports marked as available")
162
+ return True
163
+ else:
164
+ if not self.verified_ports:
165
+ logger.error("No SSH ports were verified as accessible")
166
+ return False
147
167
 
148
- logger.info(
149
- f"Successfully verified {len(self.verified_ports)} SSH ports")
150
- return True
168
+ logger.info(
169
+ f"Successfully verified {len(self.verified_ports)} SSH ports")
170
+ return True
151
171
 
152
172
  def _load_state(self) -> None:
153
173
  """Load port assignments from state file."""
@@ -203,7 +223,8 @@ class PortManager:
203
223
  return port
204
224
  else:
205
225
  # Port is in use, remove from verified ports
206
- self.verified_ports.remove(port)
226
+ if not self.skip_verification:
227
+ self.verified_ports.remove(port)
207
228
  self._used_ports.pop(vm_id)
208
229
  except Exception as e:
209
230
  logger.debug(f"Failed to check port {port}: {e}")
@@ -216,7 +237,8 @@ class PortManager:
216
237
  used_ports = self._get_used_ports()
217
238
 
218
239
  # Find first available verified port
219
- for port in sorted(self.verified_ports):
240
+ ports_to_check = sorted(self.verified_ports) if not self.skip_verification else range(self.start_port, self.end_port)
241
+ for port in ports_to_check:
220
242
  if port not in used_ports:
221
243
  # Quick check if port is actually available
222
244
  try:
@@ -230,11 +252,12 @@ class PortManager:
230
252
  self._used_ports[vm_id] = port
231
253
  self._save_state()
232
254
  logger.info(
233
- f"Allocated verified port {port} for VM {vm_id}")
255
+ f"Allocated port {port} for VM {vm_id}")
234
256
  return port
235
257
  else:
236
258
  # Port is in use, remove from verified ports
237
- self.verified_ports.remove(port)
259
+ if not self.skip_verification and port in self.verified_ports:
260
+ self.verified_ports.remove(port)
238
261
  except Exception as e:
239
262
  logger.debug(f"Failed to check port {port}: {e}")
240
263
  continue
@@ -308,11 +308,12 @@ class PythonProxyManager:
308
308
  """
309
309
  try:
310
310
  # Use provided port or allocate one
311
- if port is None:
312
- port = self.port_manager.allocate_port(vm_id)
313
- if port is None:
311
+ if port is None or port == 0:
312
+ allocated_port = self.port_manager.allocate_port(vm_id)
313
+ if allocated_port is None:
314
314
  logger.error(f"Failed to allocate port for VM {vm_id}")
315
315
  return False
316
+ port = allocated_port
316
317
 
317
318
  # Create and start proxy server
318
319
  proxy = ProxyServer(port, vm_ip)
@@ -327,7 +328,7 @@ class PythonProxyManager:
327
328
  except Exception as e:
328
329
  logger.error(f"Failed to configure proxy for VM {vm_id}: {e}")
329
330
  # Only deallocate if we allocated the port ourselves
330
- if port is None and 'port' in locals():
331
+ if 'allocated_port' in locals() and allocated_port:
331
332
  self.port_manager.deallocate_port(vm_id)
332
333
  return False
333
334
 
@@ -1,27 +0,0 @@
1
- provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
2
- provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
3
- provider/api/models.py,sha256=JOzoNf1oE5N97UqTN5xuIrTkqn2tCHqPDaIzGA3jUyo,3513
4
- provider/api/routes.py,sha256=P27RQvNqFWn6PacRwr1PaVz-yv5KAWsp9KeORejkXSI,6452
5
- provider/config.py,sha256=Aqh2cnhZ8lmfiokZuGE-P9JuF8hedL5zRVlFpANvL0c,14585
6
- provider/discovery/__init__.py,sha256=VR3NRoQtZRH5Vs8FG7jnGLR7p7wn7XeZdLaBb3t8e1g,123
7
- provider/discovery/advertiser.py,sha256=yv7RbRf1K43qOLAEa2Olj9hhN8etl2qsBuoHok0xoVs,6784
8
- provider/discovery/resource_tracker.py,sha256=8dYhJxoe_jLRwisHoA0jr575YhUKmLIqSXfW88KshcQ,6000
9
- provider/main.py,sha256=eku9O0pTifNM7OcIukoYS9fBnI0vRkeuRxQkfsW0VKU,10051
10
- provider/network/port_verifier.py,sha256=3l6WNwBHydggJRFYkAsuBp1eCxaU619kjWuM-zSVj2o,13267
11
- provider/security/ethereum.py,sha256=SDRDbcjynbVy44kNnxlDcYLL0BZ3Qnc0DvmneQ-WKLE,1383
12
- provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
13
- provider/utils/logging.py,sha256=C_elr0sJROHKQgErYpHJQvfujgh0k4Zf2gg8ZKfrmVk,2590
14
- provider/utils/port_display.py,sha256=5d_604Eo-82dqx_yV2ZScq7bKQ8IsXacc-yXC_KAz3A,11031
15
- provider/utils/retry.py,sha256=ekP2ucaSJNN-lBcrIvyHa4QYPKNITMl1a5V1X6BBvsw,1560
16
- provider/utils/setup.py,sha256=Z5dLuBQkb5vdoQsu1HJZwXmu9NWsiBYJ7Vq9-C-_tY8,2932
17
- provider/vm/__init__.py,sha256=JGs50tUmzOR1rQ_w4fMY_3XWylmiA1G7KKWZkVw51mY,501
18
- provider/vm/cloud_init.py,sha256=E5dDH7dqStRcJNDfbarBBe83-c9N63W8B5ycIrHI8eU,4627
19
- provider/vm/models.py,sha256=zkfvP5Z50SPDNajwZTt9NTDIMRQIsZLvSOsuirHEcJM,6256
20
- provider/vm/multipass.py,sha256=RufJbl39d_mEXruX0gX1zCmEEiQ_DytPXgW6F1qVQaM,16667
21
- provider/vm/name_mapper.py,sha256=MrshNeJ4Dw-WBsyiIVcn9N5xyOxaBKX4Yqhyh_m5IFg,4103
22
- provider/vm/port_manager.py,sha256=m5NZYstWt0MpFgWjzfcYj1UqQzwuFRvjkqBV_AdnxDI,10499
23
- provider/vm/proxy_manager.py,sha256=pVGb00WVg7bbFiLD90GowiwXX8zD80euz9ruzHvlgIY,14858
24
- golem_vm_provider-0.1.24.dist-info/METADATA,sha256=5XDgxlAAfze2jD9Q-7iDIT01Tn-6Dl8h3E-KFzQsuDw,10594
25
- golem_vm_provider-0.1.24.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
26
- golem_vm_provider-0.1.24.dist-info/entry_points.txt,sha256=E4rCWo_Do_2zCG_GewNuftfVlHF_8b_OvioZre0dfeA,54
27
- golem_vm_provider-0.1.24.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- golem-provider=provider.main:start
3
-