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.
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/PKG-INFO +11 -9
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/api/models.py +3 -3
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/config.py +69 -11
- golem_vm_provider-0.1.26/provider/discovery/golem_base_advertiser.py +129 -0
- golem_vm_provider-0.1.26/provider/discovery/golem_base_utils.py +10 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/main.py +89 -74
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/security/ethereum.py +9 -13
- golem_vm_provider-0.1.26/provider/security/faucet.py +132 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/port_display.py +29 -1
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/models.py +5 -5
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/multipass.py +5 -1
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/port_manager.py +45 -21
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/proxy_manager.py +12 -4
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/pyproject.toml +12 -7
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/README.md +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/__init__.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/api/__init__.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/api/routes.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/discovery/__init__.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/discovery/advertiser.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/discovery/resource_tracker.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/network/port_verifier.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/ascii_art.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/logging.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/retry.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/utils/setup.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/__init__.py +0 -0
- {golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/vm/cloud_init.py +0 -0
- {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.
|
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.
|
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.
|
24
|
-
Requires-Dist: fastapi (>=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 (>=
|
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
|
-
|
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(...,
|
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(...,
|
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
|
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("
|
37
|
-
def
|
38
|
-
"""Get
|
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
|
-
|
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(
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
67
|
-
logger.process("🔄
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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(
|
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
|
-
#
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
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(
|
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
|
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)
|
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)
|
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__(
|
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
|
-
|
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(...,
|
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
|
-
|
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(...,
|
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(...,
|
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
|
-
|
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
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
144
|
-
logger.
|
145
|
-
return
|
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
|
-
|
148
|
-
|
149
|
-
|
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.
|
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
|
-
|
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
|
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
|
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
|
-
|
306
|
-
if
|
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
|
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.
|
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:
|
23
|
+
golem-provider = "provider.main:cli"
|
24
24
|
|
25
25
|
[tool.poetry.dependencies]
|
26
|
-
python = "^3.
|
27
|
-
fastapi = "^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 = "^
|
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.
|
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 = ['
|
63
|
+
target-version = ['py313']
|
59
64
|
include = '\.pyi?$'
|
60
65
|
|
61
66
|
[tool.isort]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{golem_vm_provider-0.1.23 → golem_vm_provider-0.1.26}/provider/discovery/resource_tracker.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|