golem-vm-provider 0.1.23__tar.gz → 0.1.26__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 (29) hide show
  1. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/PKG-INFO +11 -9
  2. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/api/models.py +3 -3
  3. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/config.py +69 -11
  4. golem_vm_provider-0.1.26/provider/discovery/golem_base_advertiser.py +129 -0
  5. golem_vm_provider-0.1.26/provider/discovery/golem_base_utils.py +10 -0
  6. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/main.py +89 -74
  7. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/security/ethereum.py +9 -13
  8. golem_vm_provider-0.1.26/provider/security/faucet.py +132 -0
  9. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/port_display.py +29 -1
  10. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/models.py +5 -5
  11. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/multipass.py +5 -1
  12. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/port_manager.py +45 -21
  13. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/proxy_manager.py +12 -4
  14. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/pyproject.toml +12 -7
  15. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/README.md +0 -0
  16. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/__init__.py +0 -0
  17. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/api/__init__.py +0 -0
  18. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/api/routes.py +0 -0
  19. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/discovery/__init__.py +0 -0
  20. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/discovery/advertiser.py +0 -0
  21. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/discovery/resource_tracker.py +0 -0
  22. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/network/port_verifier.py +0 -0
  23. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/ascii_art.py +0 -0
  24. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/logging.py +0 -0
  25. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/retry.py +0 -0
  26. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/setup.py +0 -0
  27. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/__init__.py +0 -0
  28. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/cloud_init.py +0 -0
  29. {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/name_mapper.py +0 -0
@@ -1,29 +1,29 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.23
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
@@ -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):
@@ -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]
@@ -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,67 +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
-
38
- # Restore proxy configurations first
39
- logger.process("🔄 Restoring proxy configurations...")
40
- await app.state.proxy_manager._load_state()
41
-
42
- # Now initialize port manager with knowledge of restored proxies
51
+
52
+ # Initialize port manager first to verify all ports
43
53
  logger.process("🔄 Initializing port manager...")
44
54
  port_manager = PortManager(
45
55
  start_port=settings.PORT_RANGE_START,
46
56
  end_port=settings.PORT_RANGE_END,
47
57
  discovery_port=settings.PORT,
48
- existing_ports=app.state.proxy_manager.get_active_ports()
58
+ skip_verification=settings.SKIP_PORT_VERIFICATION
49
59
  )
50
-
60
+
51
61
  if not await port_manager.initialize():
52
62
  raise RuntimeError("Port verification failed")
53
-
54
- # Update provider and proxy manager with verified port manager
63
+
64
+ # Store port manager references
55
65
  app.state.port_manager = port_manager
56
66
  provider.port_manager = port_manager
57
67
  app.state.proxy_manager.port_manager = port_manager
58
-
68
+
69
+ # Now restore proxy configurations using only verified ports
70
+ logger.process("🔄 Restoring proxy configurations...")
71
+ await app.state.proxy_manager._load_state()
72
+
59
73
  except asyncio.TimeoutError:
60
74
  logger.error("Provider initialization timed out")
61
75
  raise
62
76
  except Exception as e:
63
77
  logger.error(f"Failed to initialize provider: {e}")
64
78
  raise
65
-
66
- # Create and start advertiser in background
67
- logger.process("🔄 Starting resource advertiser...")
68
- advertiser = ResourceAdvertiser(
69
- resource_tracker=resource_tracker,
70
- discovery_url=settings.DISCOVERY_URL,
71
- provider_id=settings.PROVIDER_ID
72
- )
73
-
74
- # Start advertiser in background task
75
- 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
+ )
76
93
  app.state.advertiser = advertiser
77
-
78
- logger.success("✨ Provider setup complete and ready to accept requests")
94
+
95
+ logger.success(
96
+ "✨ Provider setup complete and ready to accept requests")
79
97
  except Exception as e:
80
98
  logger.error(f"Failed to setup provider: {e}")
81
99
  # Attempt cleanup of any initialized components
82
100
  await cleanup_provider()
83
101
  raise
84
102
 
103
+
85
104
  async def cleanup_provider() -> None:
86
105
  """Cleanup provider components."""
87
106
  cleanup_errors = []
88
-
107
+
89
108
  # Stop advertiser
90
109
  if hasattr(app.state, "advertiser"):
91
110
  try:
@@ -98,7 +117,7 @@ async def cleanup_provider() -> None:
98
117
  pass
99
118
  except Exception as e:
100
119
  cleanup_errors.append(f"Failed to stop advertiser: {e}")
101
-
120
+
102
121
  # Cleanup proxy manager first to stop all proxy servers
103
122
  if hasattr(app.state, "proxy_manager"):
104
123
  try:
@@ -107,7 +126,7 @@ async def cleanup_provider() -> None:
107
126
  cleanup_errors.append("Proxy manager cleanup timed out")
108
127
  except Exception as e:
109
128
  cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
110
-
129
+
111
130
  # Cleanup provider
112
131
  if hasattr(app.state, "provider"):
113
132
  try:
@@ -116,71 +135,56 @@ async def cleanup_provider() -> None:
116
135
  cleanup_errors.append("Provider cleanup timed out")
117
136
  except Exception as e:
118
137
  cleanup_errors.append(f"Failed to cleanup provider: {e}")
119
-
138
+
120
139
  if cleanup_errors:
121
140
  error_msg = "\n".join(cleanup_errors)
122
141
  logger.error(f"Errors during cleanup:\n{error_msg}")
123
142
  else:
124
143
  logger.success("✨ Provider cleanup complete")
125
144
 
145
+
126
146
  @app.on_event("startup")
127
147
  async def startup_event():
128
148
  """Handle application startup."""
129
149
  try:
130
150
  # Display startup animation
131
151
  await startup_animation()
152
+
153
+ # Initialize provider
154
+ await setup_provider()
132
155
 
133
- # Verify ports first
134
- from .vm.port_manager import PortManager
135
- from .utils.port_display import PortVerificationDisplay
136
- from .config import settings
137
-
138
- display = PortVerificationDisplay(
139
- provider_port=settings.PORT,
140
- port_range_start=settings.PORT_RANGE_START,
141
- port_range_end=settings.PORT_RANGE_END
142
- )
143
- display.print_header()
144
-
145
- # Initialize port manager
146
- logger.process("🔄 Verifying port accessibility...")
147
- port_manager = PortManager(
148
- start_port=settings.PORT_RANGE_START,
149
- end_port=settings.PORT_RANGE_END,
150
- 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,
151
161
  )
152
- if not await port_manager.initialize():
153
- logger.error("Port verification failed. Please ensure:")
154
- logger.error(f"1. Port {settings.PORT} is accessible for provider access")
155
- logger.error(f"2. Some ports in range {settings.PORT_RANGE_START}-{settings.PORT_RANGE_END} are accessible for VM access")
156
- logger.error("3. Your firewall/router is properly configured")
157
- raise RuntimeError("Port verification failed")
158
-
159
- logger.success(f"✅ Port verification successful - {len(port_manager.verified_ports)} ports available")
160
-
161
- # Store port manager in app state for later use
162
- 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())
163
168
 
164
- # Initialize provider
165
- await setup_provider()
166
169
  except Exception as e:
167
170
  logger.error(f"Startup failed: {e}")
168
171
  # Ensure proper cleanup
169
172
  await cleanup_provider()
170
173
  raise
171
174
 
175
+
172
176
  @app.on_event("shutdown")
173
177
  async def shutdown_event():
174
178
  """Handle application shutdown."""
175
179
  await cleanup_provider()
176
180
 
177
181
  # Import routes after app creation to avoid circular imports
178
- from .api import routes
179
182
  app.include_router(routes.router, prefix="/api/v1")
180
183
 
181
184
  # Export app for uvicorn
182
185
  __all__ = ["app", "start"]
183
186
 
187
+
184
188
  def check_requirements():
185
189
  """Check if all requirements are met."""
186
190
  try:
@@ -191,12 +195,13 @@ def check_requirements():
191
195
  logger.error(f"Requirements check failed: {e}")
192
196
  return False
193
197
 
198
+
194
199
  async def verify_provider_port(port: int) -> bool:
195
200
  """Verify that the provider port is available for binding.
196
-
201
+
197
202
  Args:
198
203
  port: The port to verify
199
-
204
+
200
205
  Returns:
201
206
  bool: True if the port is available, False otherwise
202
207
  """
@@ -219,7 +224,13 @@ async def verify_provider_port(port: int) -> bool:
219
224
  logger.error("3. Your firewall allows binding to this port")
220
225
  return False
221
226
 
222
- 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.")):
223
234
  """Start the provider server."""
224
235
  import sys
225
236
  from pathlib import Path
@@ -227,15 +238,15 @@ def start():
227
238
  import uvicorn
228
239
  from .utils.logging import setup_logger
229
240
  from .config import settings
230
-
241
+
231
242
  # Configure logging with debug mode
232
243
  logger = setup_logger(__name__, debug=True)
233
-
244
+
234
245
  try:
235
246
  # Load environment variables from .env file
236
247
  env_path = Path(__file__).parent.parent / '.env'
237
248
  load_dotenv(dotenv_path=env_path)
238
-
249
+
239
250
  # Log environment variables
240
251
  logger.info("Environment variables:")
241
252
  for key, value in os.environ.items():
@@ -246,18 +257,19 @@ def start():
246
257
  if not check_requirements():
247
258
  logger.error("Requirements check failed")
248
259
  sys.exit(1)
249
-
260
+
250
261
  # Verify provider port is available
251
- if not asyncio.run(verify_provider_port(settings.PORT)):
262
+ if not no_verify_port and not asyncio.run(verify_provider_port(settings.PORT)):
252
263
  logger.error(f"Provider port {settings.PORT} is not available")
253
264
  sys.exit(1)
254
-
265
+
255
266
  # Configure uvicorn logging
256
267
  log_config = uvicorn.config.LOGGING_CONFIG
257
268
  log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
258
-
269
+
259
270
  # Run server
260
- logger.process(f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
271
+ logger.process(
272
+ f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
261
273
  uvicorn.run(
262
274
  "provider:app",
263
275
  host=settings.HOST,
@@ -271,3 +283,6 @@ def start():
271
283
  except Exception as e:
272
284
  logger.error(f"Failed to start provider server: {e}")
273
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")
@@ -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
 
@@ -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,19 +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
- # Only verify ports that aren't already marked as in use
84
- available_ports = set(range(self.start_port, self.end_port)) - self._existing_ports
85
- ssh_ports = list(available_ports)
86
- logger.info(f"Starting port verification...")
87
- logger.info(f"SSH ports range: {self.start_port}-{self.end_port}")
88
- logger.info(
89
- f"Using port check servers: {', '.join(self.port_check_servers)}")
90
-
91
- 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)
92
109
 
93
110
  # Add provider port as verified since we already checked it
94
111
  results[self.discovery_port] = PortVerificationResult(
@@ -140,13 +157,17 @@ class PortManager:
140
157
  # Print precise summary of current status
141
158
  display.print_summary(discovery_result, ssh_results)
142
159
 
143
- if not self.verified_ports:
144
- logger.error("No SSH ports were verified as accessible")
145
- 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
146
167
 
147
- logger.info(
148
- f"Successfully verified {len(self.verified_ports)} SSH ports")
149
- return True
168
+ logger.info(
169
+ f"Successfully verified {len(self.verified_ports)} SSH ports")
170
+ return True
150
171
 
151
172
  def _load_state(self) -> None:
152
173
  """Load port assignments from state file."""
@@ -202,7 +223,8 @@ class PortManager:
202
223
  return port
203
224
  else:
204
225
  # Port is in use, remove from verified ports
205
- self.verified_ports.remove(port)
226
+ if not self.skip_verification:
227
+ self.verified_ports.remove(port)
206
228
  self._used_ports.pop(vm_id)
207
229
  except Exception as e:
208
230
  logger.debug(f"Failed to check port {port}: {e}")
@@ -215,7 +237,8 @@ class PortManager:
215
237
  used_ports = self._get_used_ports()
216
238
 
217
239
  # Find first available verified port
218
- 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:
219
242
  if port not in used_ports:
220
243
  # Quick check if port is actually available
221
244
  try:
@@ -229,11 +252,12 @@ class PortManager:
229
252
  self._used_ports[vm_id] = port
230
253
  self._save_state()
231
254
  logger.info(
232
- f"Allocated verified port {port} for VM {vm_id}")
255
+ f"Allocated port {port} for VM {vm_id}")
233
256
  return port
234
257
  else:
235
258
  # Port is in use, remove from verified ports
236
- self.verified_ports.remove(port)
259
+ if not self.skip_verification and port in self.verified_ports:
260
+ self.verified_ports.remove(port)
237
261
  except Exception as e:
238
262
  logger.debug(f"Failed to check port {port}: {e}")
239
263
  continue
@@ -243,6 +243,13 @@ class PythonProxyManager:
243
243
  await asyncio.sleep(delay)
244
244
  delay *= 2 # Exponential backoff
245
245
 
246
+ # Check if port is verified before restoring
247
+ if not self.port_manager or port not in self.port_manager.verified_ports:
248
+ logger.warning(f"Port {port} is not verified, skipping proxy restoration for {multipass_name}")
249
+ # Remove from active ports since we can't restore it
250
+ self._active_ports.pop(multipass_name, None)
251
+ return False
252
+
246
253
  # Attempt to create proxy
247
254
  proxy = ProxyServer(port, vm_ip)
248
255
  await proxy.start()
@@ -301,11 +308,12 @@ class PythonProxyManager:
301
308
  """
302
309
  try:
303
310
  # Use provided port or allocate one
304
- if port is None:
305
- port = self.port_manager.allocate_port(vm_id)
306
- 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:
307
314
  logger.error(f"Failed to allocate port for VM {vm_id}")
308
315
  return False
316
+ port = allocated_port
309
317
 
310
318
  # Create and start proxy server
311
319
  proxy = ProxyServer(port, vm_ip)
@@ -320,7 +328,7 @@ class PythonProxyManager:
320
328
  except Exception as e:
321
329
  logger.error(f"Failed to configure proxy for VM {vm_id}: {e}")
322
330
  # Only deallocate if we allocated the port ourselves
323
- if port is None and 'port' in locals():
331
+ if 'allocated_port' in locals() and allocated_port:
324
332
  self.port_manager.deallocate_port(vm_id)
325
333
  return False
326
334
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "golem-vm-provider"
3
- version = "0.1.23"
3
+ version = "0.1.26"
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"
@@ -20,25 +20,30 @@ packages = [
20
20
  ]
21
21
 
22
22
  [tool.poetry.scripts]
23
- golem-provider = "provider.main:start"
23
+ golem-provider = "provider.main:cli"
24
24
 
25
25
  [tool.poetry.dependencies]
26
- python = "^3.9"
27
- fastapi = "^0.68.0"
26
+ python = "^3.13"
27
+ fastapi = "^0.103.0"
28
28
  uvicorn = "^0.15.0"
29
29
  aiohttp = "^3.8.1"
30
30
  psutil = "^5.9.0"
31
- pydantic = "^1.8.2"
31
+ pydantic = "^2.4.0"
32
+ pydantic-settings = "^2.1.0"
32
33
  python-multipart = "^0.0.5"
33
34
  python-jose = {extras = ["cryptography"], version = "^3.3.0"}
34
35
  cryptography = "^3.4.7"
35
36
  python-dotenv = "^1.0.0"
36
37
  pyyaml = "^6.0.1"
37
38
  requests = "^2.31.0"
38
- eth-account = "^0.8.0"
39
+ eth-account = "^0.13.6"
39
40
  setuptools = "^69.0.3"
40
41
  colorlog = "^6.8.0"
41
42
  rich = "^13.7.0"
43
+ httpx = "^0.23.0"
44
+ typer = "^0.4.0"
45
+ web3 = "==7.13.0"
46
+ golem-base-sdk = "==0.1.0"
42
47
 
43
48
  [tool.poetry.group.dev.dependencies]
44
49
  pytest = "^7.0.0"
@@ -55,7 +60,7 @@ build-backend = "poetry.core.masonry.api"
55
60
 
56
61
  [tool.black]
57
62
  line-length = 88
58
- target-version = ['py39']
63
+ target-version = ['py313']
59
64
  include = '\.pyi?$'
60
65
 
61
66
  [tool.isort]