golem-vm-provider 0.1.24__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.24 → golem_vm_provider-0.1.26}/PKG-INFO +11 -9
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/api/models.py +3 -3
- {golem_vm_provider-0.1.24 → 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.24 → golem_vm_provider-0.1.26}/provider/main.py +85 -69
- {golem_vm_provider-0.1.24 → 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.24 → golem_vm_provider-0.1.26}/provider/utils/port_display.py +29 -1
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/vm/models.py +5 -5
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/vm/multipass.py +5 -1
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/vm/port_manager.py +45 -22
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/vm/proxy_manager.py +5 -4
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/pyproject.toml +12 -7
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/README.md +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/__init__.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/api/__init__.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/api/routes.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/discovery/__init__.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/discovery/advertiser.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/discovery/resource_tracker.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/network/port_verifier.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/utils/ascii_art.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/utils/logging.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/utils/retry.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/utils/setup.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/vm/__init__.py +0 -0
- {golem_vm_provider-0.1.24 → golem_vm_provider-0.1.26}/provider/vm/cloud_init.py +0 -0
- {golem_vm_provider-0.1.24 → 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,66 +38,73 @@ async def setup_provider() -> None:
|
|
25
38
|
|
26
39
|
# Create provider with resource tracker and temporary port manager
|
27
40
|
logger.process("🔄 Initializing VM provider...")
|
28
|
-
provider = MultipassProvider(
|
29
|
-
|
41
|
+
provider = MultipassProvider(
|
42
|
+
resource_tracker, port_manager=None) # Will be set later
|
43
|
+
|
30
44
|
try:
|
31
45
|
# Initialize provider (without port operations)
|
32
46
|
await asyncio.wait_for(provider.initialize(), timeout=30)
|
33
|
-
|
47
|
+
|
34
48
|
# Store provider reference
|
35
49
|
app.state.provider = provider
|
36
50
|
app.state.proxy_manager = provider.proxy_manager
|
37
|
-
|
51
|
+
|
38
52
|
# Initialize port manager first to verify all ports
|
39
53
|
logger.process("🔄 Initializing port manager...")
|
40
54
|
port_manager = PortManager(
|
41
55
|
start_port=settings.PORT_RANGE_START,
|
42
56
|
end_port=settings.PORT_RANGE_END,
|
43
|
-
discovery_port=settings.PORT
|
57
|
+
discovery_port=settings.PORT,
|
58
|
+
skip_verification=settings.SKIP_PORT_VERIFICATION
|
44
59
|
)
|
45
|
-
|
60
|
+
|
46
61
|
if not await port_manager.initialize():
|
47
62
|
raise RuntimeError("Port verification failed")
|
48
|
-
|
63
|
+
|
49
64
|
# Store port manager references
|
50
65
|
app.state.port_manager = port_manager
|
51
66
|
provider.port_manager = port_manager
|
52
67
|
app.state.proxy_manager.port_manager = port_manager
|
53
|
-
|
68
|
+
|
54
69
|
# Now restore proxy configurations using only verified ports
|
55
70
|
logger.process("🔄 Restoring proxy configurations...")
|
56
71
|
await app.state.proxy_manager._load_state()
|
57
|
-
|
72
|
+
|
58
73
|
except asyncio.TimeoutError:
|
59
74
|
logger.error("Provider initialization timed out")
|
60
75
|
raise
|
61
76
|
except Exception as e:
|
62
77
|
logger.error(f"Failed to initialize provider: {e}")
|
63
78
|
raise
|
64
|
-
|
65
|
-
# Create
|
66
|
-
logger.process("🔄
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
79
|
+
|
80
|
+
# Create advertiser
|
81
|
+
logger.process("🔄 Initializing resource advertiser...")
|
82
|
+
if settings.DISCOVERY_DRIVER == "golem-base":
|
83
|
+
advertiser = GolemBaseAdvertiser(
|
84
|
+
resource_tracker=resource_tracker
|
85
|
+
)
|
86
|
+
await advertiser.initialize()
|
87
|
+
else:
|
88
|
+
advertiser = ResourceAdvertiser(
|
89
|
+
resource_tracker=resource_tracker,
|
90
|
+
discovery_url=settings.DISCOVERY_URL,
|
91
|
+
provider_id=settings.PROVIDER_ID
|
92
|
+
)
|
75
93
|
app.state.advertiser = advertiser
|
76
|
-
|
77
|
-
logger.success(
|
94
|
+
|
95
|
+
logger.success(
|
96
|
+
"✨ Provider setup complete and ready to accept requests")
|
78
97
|
except Exception as e:
|
79
98
|
logger.error(f"Failed to setup provider: {e}")
|
80
99
|
# Attempt cleanup of any initialized components
|
81
100
|
await cleanup_provider()
|
82
101
|
raise
|
83
102
|
|
103
|
+
|
84
104
|
async def cleanup_provider() -> None:
|
85
105
|
"""Cleanup provider components."""
|
86
106
|
cleanup_errors = []
|
87
|
-
|
107
|
+
|
88
108
|
# Stop advertiser
|
89
109
|
if hasattr(app.state, "advertiser"):
|
90
110
|
try:
|
@@ -97,7 +117,7 @@ async def cleanup_provider() -> None:
|
|
97
117
|
pass
|
98
118
|
except Exception as e:
|
99
119
|
cleanup_errors.append(f"Failed to stop advertiser: {e}")
|
100
|
-
|
120
|
+
|
101
121
|
# Cleanup proxy manager first to stop all proxy servers
|
102
122
|
if hasattr(app.state, "proxy_manager"):
|
103
123
|
try:
|
@@ -106,7 +126,7 @@ async def cleanup_provider() -> None:
|
|
106
126
|
cleanup_errors.append("Proxy manager cleanup timed out")
|
107
127
|
except Exception as e:
|
108
128
|
cleanup_errors.append(f"Failed to cleanup proxy manager: {e}")
|
109
|
-
|
129
|
+
|
110
130
|
# Cleanup provider
|
111
131
|
if hasattr(app.state, "provider"):
|
112
132
|
try:
|
@@ -115,71 +135,56 @@ async def cleanup_provider() -> None:
|
|
115
135
|
cleanup_errors.append("Provider cleanup timed out")
|
116
136
|
except Exception as e:
|
117
137
|
cleanup_errors.append(f"Failed to cleanup provider: {e}")
|
118
|
-
|
138
|
+
|
119
139
|
if cleanup_errors:
|
120
140
|
error_msg = "\n".join(cleanup_errors)
|
121
141
|
logger.error(f"Errors during cleanup:\n{error_msg}")
|
122
142
|
else:
|
123
143
|
logger.success("✨ Provider cleanup complete")
|
124
144
|
|
145
|
+
|
125
146
|
@app.on_event("startup")
|
126
147
|
async def startup_event():
|
127
148
|
"""Handle application startup."""
|
128
149
|
try:
|
129
150
|
# Display startup animation
|
130
151
|
await startup_animation()
|
152
|
+
|
153
|
+
# Initialize provider
|
154
|
+
await setup_provider()
|
131
155
|
|
132
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
display = PortVerificationDisplay(
|
138
|
-
provider_port=settings.PORT,
|
139
|
-
port_range_start=settings.PORT_RANGE_START,
|
140
|
-
port_range_end=settings.PORT_RANGE_END
|
141
|
-
)
|
142
|
-
display.print_header()
|
143
|
-
|
144
|
-
# Initialize port manager
|
145
|
-
logger.process("🔄 Verifying port accessibility...")
|
146
|
-
port_manager = PortManager(
|
147
|
-
start_port=settings.PORT_RANGE_START,
|
148
|
-
end_port=settings.PORT_RANGE_END,
|
149
|
-
discovery_port=settings.PORT
|
156
|
+
# Check wallet balance and request funds if needed
|
157
|
+
faucet_client = FaucetClient(
|
158
|
+
faucet_url=settings.FAUCET_URL,
|
159
|
+
captcha_url=settings.CAPTCHA_URL,
|
160
|
+
captcha_api_key=settings.CAPTCHA_API_KEY,
|
150
161
|
)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
logger.success(f"✅ Port verification successful - {len(port_manager.verified_ports)} ports available")
|
159
|
-
|
160
|
-
# Store port manager in app state for later use
|
161
|
-
app.state.port_manager = port_manager
|
162
|
+
await faucet_client.get_funds(settings.PROVIDER_ID)
|
163
|
+
|
164
|
+
# Post initial advertisement and start advertising loop
|
165
|
+
if isinstance(app.state.advertiser, GolemBaseAdvertiser):
|
166
|
+
await app.state.advertiser.post_advertisement()
|
167
|
+
app.state.advertiser_task = asyncio.create_task(app.state.advertiser.start_loop())
|
162
168
|
|
163
|
-
# Initialize provider
|
164
|
-
await setup_provider()
|
165
169
|
except Exception as e:
|
166
170
|
logger.error(f"Startup failed: {e}")
|
167
171
|
# Ensure proper cleanup
|
168
172
|
await cleanup_provider()
|
169
173
|
raise
|
170
174
|
|
175
|
+
|
171
176
|
@app.on_event("shutdown")
|
172
177
|
async def shutdown_event():
|
173
178
|
"""Handle application shutdown."""
|
174
179
|
await cleanup_provider()
|
175
180
|
|
176
181
|
# Import routes after app creation to avoid circular imports
|
177
|
-
from .api import routes
|
178
182
|
app.include_router(routes.router, prefix="/api/v1")
|
179
183
|
|
180
184
|
# Export app for uvicorn
|
181
185
|
__all__ = ["app", "start"]
|
182
186
|
|
187
|
+
|
183
188
|
def check_requirements():
|
184
189
|
"""Check if all requirements are met."""
|
185
190
|
try:
|
@@ -190,12 +195,13 @@ def check_requirements():
|
|
190
195
|
logger.error(f"Requirements check failed: {e}")
|
191
196
|
return False
|
192
197
|
|
198
|
+
|
193
199
|
async def verify_provider_port(port: int) -> bool:
|
194
200
|
"""Verify that the provider port is available for binding.
|
195
|
-
|
201
|
+
|
196
202
|
Args:
|
197
203
|
port: The port to verify
|
198
|
-
|
204
|
+
|
199
205
|
Returns:
|
200
206
|
bool: True if the port is available, False otherwise
|
201
207
|
"""
|
@@ -218,7 +224,13 @@ async def verify_provider_port(port: int) -> bool:
|
|
218
224
|
logger.error("3. Your firewall allows binding to this port")
|
219
225
|
return False
|
220
226
|
|
221
|
-
|
227
|
+
|
228
|
+
import typer
|
229
|
+
|
230
|
+
cli = typer.Typer()
|
231
|
+
|
232
|
+
@cli.command()
|
233
|
+
def start(no_verify_port: bool = typer.Option(False, "--no-verify-port", help="Skip provider port verification.")):
|
222
234
|
"""Start the provider server."""
|
223
235
|
import sys
|
224
236
|
from pathlib import Path
|
@@ -226,15 +238,15 @@ def start():
|
|
226
238
|
import uvicorn
|
227
239
|
from .utils.logging import setup_logger
|
228
240
|
from .config import settings
|
229
|
-
|
241
|
+
|
230
242
|
# Configure logging with debug mode
|
231
243
|
logger = setup_logger(__name__, debug=True)
|
232
|
-
|
244
|
+
|
233
245
|
try:
|
234
246
|
# Load environment variables from .env file
|
235
247
|
env_path = Path(__file__).parent.parent / '.env'
|
236
248
|
load_dotenv(dotenv_path=env_path)
|
237
|
-
|
249
|
+
|
238
250
|
# Log environment variables
|
239
251
|
logger.info("Environment variables:")
|
240
252
|
for key, value in os.environ.items():
|
@@ -245,18 +257,19 @@ def start():
|
|
245
257
|
if not check_requirements():
|
246
258
|
logger.error("Requirements check failed")
|
247
259
|
sys.exit(1)
|
248
|
-
|
260
|
+
|
249
261
|
# Verify provider port is available
|
250
|
-
if not asyncio.run(verify_provider_port(settings.PORT)):
|
262
|
+
if not no_verify_port and not asyncio.run(verify_provider_port(settings.PORT)):
|
251
263
|
logger.error(f"Provider port {settings.PORT} is not available")
|
252
264
|
sys.exit(1)
|
253
|
-
|
265
|
+
|
254
266
|
# Configure uvicorn logging
|
255
267
|
log_config = uvicorn.config.LOGGING_CONFIG
|
256
268
|
log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
257
|
-
|
269
|
+
|
258
270
|
# Run server
|
259
|
-
logger.process(
|
271
|
+
logger.process(
|
272
|
+
f"🚀 Starting provider server on {settings.HOST}:{settings.PORT}")
|
260
273
|
uvicorn.run(
|
261
274
|
"provider:app",
|
262
275
|
host=settings.HOST,
|
@@ -270,3 +283,6 @@ def start():
|
|
270
283
|
except Exception as e:
|
271
284
|
logger.error(f"Failed to start provider server: {e}")
|
272
285
|
sys.exit(1)
|
286
|
+
|
287
|
+
if __name__ == "__main__":
|
288
|
+
cli()
|
@@ -13,29 +13,25 @@ class EthereumIdentity:
|
|
13
13
|
self.key_dir = Path(key_dir)
|
14
14
|
self.key_file = self.key_dir / "provider_key.json"
|
15
15
|
|
16
|
-
def get_or_create_identity(self) -> str:
|
17
|
-
"""Get existing provider ID or create new one
|
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,20 +78,34 @@ class PortManager:
|
|
76
78
|
display = PortVerificationDisplay(
|
77
79
|
provider_port=self.discovery_port,
|
78
80
|
port_range_start=self.start_port,
|
79
|
-
port_range_end=self.end_port
|
81
|
+
port_range_end=self.end_port,
|
82
|
+
skip_verification=self.skip_verification
|
80
83
|
)
|
81
84
|
display.print_header()
|
82
85
|
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
86
|
+
# If verification is skipped, mark all ports as verified
|
87
|
+
if self.skip_verification:
|
88
|
+
logger.warning("⚠️ Port verification is disabled in development mode")
|
89
|
+
logger.warning(" All ports will be considered available")
|
90
|
+
logger.warning(" This should only be used for development/testing")
|
91
|
+
|
92
|
+
# Mark all ports as verified
|
93
|
+
self.verified_ports = set(range(self.start_port, self.end_port))
|
94
|
+
|
95
|
+
# In development mode, we don't need to create any results
|
96
|
+
# The display will handle development mode differently
|
97
|
+
results = {}
|
98
|
+
else:
|
99
|
+
# Verify all ports in range, including existing ones
|
100
|
+
ssh_ports = list(range(self.start_port, self.end_port))
|
101
|
+
logger.info(f"Starting port verification...")
|
102
|
+
logger.info(f"SSH ports range: {self.start_port}-{self.end_port}")
|
103
|
+
logger.info(
|
104
|
+
f"Using port check servers: {', '.join(self.port_check_servers)}")
|
105
|
+
|
106
|
+
# Clear existing verified ports before verification
|
107
|
+
self.verified_ports.clear()
|
108
|
+
results = await self.port_verifier.verify_ports(ssh_ports)
|
93
109
|
|
94
110
|
# Add provider port as verified since we already checked it
|
95
111
|
results[self.discovery_port] = PortVerificationResult(
|
@@ -141,13 +157,17 @@ class PortManager:
|
|
141
157
|
# Print precise summary of current status
|
142
158
|
display.print_summary(discovery_result, ssh_results)
|
143
159
|
|
144
|
-
if
|
145
|
-
logger.
|
146
|
-
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
|
147
167
|
|
148
|
-
|
149
|
-
|
150
|
-
|
168
|
+
logger.info(
|
169
|
+
f"Successfully verified {len(self.verified_ports)} SSH ports")
|
170
|
+
return True
|
151
171
|
|
152
172
|
def _load_state(self) -> None:
|
153
173
|
"""Load port assignments from state file."""
|
@@ -203,7 +223,8 @@ class PortManager:
|
|
203
223
|
return port
|
204
224
|
else:
|
205
225
|
# Port is in use, remove from verified ports
|
206
|
-
self.
|
226
|
+
if not self.skip_verification:
|
227
|
+
self.verified_ports.remove(port)
|
207
228
|
self._used_ports.pop(vm_id)
|
208
229
|
except Exception as e:
|
209
230
|
logger.debug(f"Failed to check port {port}: {e}")
|
@@ -216,7 +237,8 @@ class PortManager:
|
|
216
237
|
used_ports = self._get_used_ports()
|
217
238
|
|
218
239
|
# Find first available verified port
|
219
|
-
|
240
|
+
ports_to_check = sorted(self.verified_ports) if not self.skip_verification else range(self.start_port, self.end_port)
|
241
|
+
for port in ports_to_check:
|
220
242
|
if port not in used_ports:
|
221
243
|
# Quick check if port is actually available
|
222
244
|
try:
|
@@ -230,11 +252,12 @@ class PortManager:
|
|
230
252
|
self._used_ports[vm_id] = port
|
231
253
|
self._save_state()
|
232
254
|
logger.info(
|
233
|
-
f"Allocated
|
255
|
+
f"Allocated port {port} for VM {vm_id}")
|
234
256
|
return port
|
235
257
|
else:
|
236
258
|
# Port is in use, remove from verified ports
|
237
|
-
self.verified_ports
|
259
|
+
if not self.skip_verification and port in self.verified_ports:
|
260
|
+
self.verified_ports.remove(port)
|
238
261
|
except Exception as e:
|
239
262
|
logger.debug(f"Failed to check port {port}: {e}")
|
240
263
|
continue
|
@@ -308,11 +308,12 @@ class PythonProxyManager:
|
|
308
308
|
"""
|
309
309
|
try:
|
310
310
|
# Use provided port or allocate one
|
311
|
-
if port is None:
|
312
|
-
|
313
|
-
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:
|
314
314
|
logger.error(f"Failed to allocate port for VM {vm_id}")
|
315
315
|
return False
|
316
|
+
port = allocated_port
|
316
317
|
|
317
318
|
# Create and start proxy server
|
318
319
|
proxy = ProxyServer(port, vm_ip)
|
@@ -327,7 +328,7 @@ class PythonProxyManager:
|
|
327
328
|
except Exception as e:
|
328
329
|
logger.error(f"Failed to configure proxy for VM {vm_id}: {e}")
|
329
330
|
# Only deallocate if we allocated the port ourselves
|
330
|
-
if
|
331
|
+
if 'allocated_port' in locals() and allocated_port:
|
331
332
|
self.port_manager.deallocate_port(vm_id)
|
332
333
|
return False
|
333
334
|
|
@@ -1,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.24 → 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
|