golem-vm-provider 0.1.24__py3-none-any.whl → 0.1.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {golem_vm_provider-0.1.24.dist-info → golem_vm_provider-0.1.27.dist-info}/METADATA +17 -10
- golem_vm_provider-0.1.27.dist-info/RECORD +38 -0
- {golem_vm_provider-0.1.24.dist-info → golem_vm_provider-0.1.27.dist-info}/WHEEL +1 -1
- golem_vm_provider-0.1.27.dist-info/entry_points.txt +4 -0
- provider/api/models.py +10 -10
- provider/api/routes.py +89 -95
- provider/config.py +101 -21
- provider/container.py +84 -0
- provider/discovery/__init__.py +8 -2
- provider/discovery/advertiser.py +41 -63
- provider/discovery/golem_base_advertiser.py +135 -0
- provider/discovery/golem_base_utils.py +10 -0
- provider/discovery/resource_monitor.py +34 -0
- provider/discovery/resource_tracker.py +1 -1
- provider/discovery/service.py +24 -0
- provider/main.py +88 -171
- provider/security/ethereum.py +9 -13
- provider/security/faucet.py +132 -0
- provider/service.py +67 -0
- provider/utils/__init__.py +0 -0
- provider/utils/logging.py +11 -27
- provider/utils/port_display.py +27 -1
- provider/utils/retry.py +39 -0
- provider/vm/__init__.py +1 -1
- provider/vm/models.py +13 -12
- provider/vm/multipass.py +2 -416
- provider/vm/multipass_adapter.py +221 -0
- provider/vm/name_mapper.py +5 -5
- provider/vm/port_manager.py +67 -26
- provider/vm/provider.py +48 -0
- provider/vm/proxy_manager.py +4 -3
- provider/vm/service.py +91 -0
- golem_vm_provider-0.1.24.dist-info/RECORD +0 -27
- golem_vm_provider-0.1.24.dist-info/entry_points.txt +0 -3
@@ -1,29 +1,30 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: golem-vm-provider
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.27
|
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:
|
24
|
-
Requires-Dist:
|
20
|
+
Requires-Dist: dependency-injector (>=4.41.0,<5.0.0)
|
21
|
+
Requires-Dist: eth-account (>=0.13.6,<0.14.0)
|
22
|
+
Requires-Dist: fastapi (>=0.103.0,<0.104.0)
|
23
|
+
Requires-Dist: golem-base-sdk (==0.1.0)
|
24
|
+
Requires-Dist: httpx (>=0.23.0,<0.24.0)
|
25
25
|
Requires-Dist: psutil (>=5.9.0,<6.0.0)
|
26
|
-
Requires-Dist: pydantic (>=
|
26
|
+
Requires-Dist: pydantic (>=2.4.0,<3.0.0)
|
27
|
+
Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
|
27
28
|
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
28
29
|
Requires-Dist: python-jose[cryptography] (>=3.3.0,<4.0.0)
|
29
30
|
Requires-Dist: python-multipart (>=0.0.5,<0.0.6)
|
@@ -31,7 +32,9 @@ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
31
32
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
32
33
|
Requires-Dist: rich (>=13.7.0,<14.0.0)
|
33
34
|
Requires-Dist: setuptools (>=69.0.3,<70.0.0)
|
35
|
+
Requires-Dist: typer (>=0.4.0,<0.5.0)
|
34
36
|
Requires-Dist: uvicorn (>=0.15.0,<0.16.0)
|
37
|
+
Requires-Dist: web3 (==7.13.0)
|
35
38
|
Project-URL: Homepage, https://github.com/cryptobench/vm-on-golem
|
36
39
|
Project-URL: Repository, https://github.com/cryptobench/vm-on-golem
|
37
40
|
Description-Content-Type: text/markdown
|
@@ -273,7 +276,11 @@ Response:
|
|
273
276
|
### Starting the Provider
|
274
277
|
|
275
278
|
```bash
|
276
|
-
|
279
|
+
# To run in production mode
|
280
|
+
poetry run golem-provider start
|
281
|
+
|
282
|
+
# To run in development mode
|
283
|
+
poetry run golem-provider dev
|
277
284
|
```
|
278
285
|
|
279
286
|
The provider will:
|
@@ -0,0 +1,38 @@
|
|
1
|
+
provider/__init__.py,sha256=HO1fkPpZqPO3z8O8-eVIyx8xXSMIVuTR_b1YF0RtXOg,45
|
2
|
+
provider/api/__init__.py,sha256=ssX1ugDqEPt8Fn04IymgmG-Ev8PiXLsCSaiZVvHQnec,344
|
3
|
+
provider/api/models.py,sha256=hTQYzVZHJ-SD_pIpoV0KbPUghR-PUY9YKyUlETstwuQ,3567
|
4
|
+
provider/api/routes.py,sha256=0_bh3Oh_5f__9wX6Fqb2Hray4YZAStzmlienyV__Sbs,5459
|
5
|
+
provider/config.py,sha256=9POtsNbRYDcVW1XGTv6szosKOR5h-41-lHXnJ2_5coM,17856
|
6
|
+
provider/container.py,sha256=u8A1FuG2targtBBqQlSiPW1yCTOm1tbrRQh8sIHstiM,2442
|
7
|
+
provider/discovery/__init__.py,sha256=Y6o8RxGevBpuQS3k32y-zSVbP6HBXG3veBl9ElVPKaU,349
|
8
|
+
provider/discovery/advertiser.py,sha256=2-khnnzaaftiV1QfJoILkzX-ipa3m1PxSdNd7gy7Y0A,5672
|
9
|
+
provider/discovery/golem_base_advertiser.py,sha256=qXIiQ0nvn7ytb6LR9KaMNFwGSE2HTY4XtMlkkAG7pj4,6093
|
10
|
+
provider/discovery/golem_base_utils.py,sha256=xk7vznhMgzrn0AuGyk6-9N9ukp9oPdBbbk1RI-sVjp0,607
|
11
|
+
provider/discovery/resource_monitor.py,sha256=AmiEc7yBGEGXCunQ-QKmVgosDX3gOhK1Y58LJZXrwAs,949
|
12
|
+
provider/discovery/resource_tracker.py,sha256=MP7IXd3aIMsjB4xz5Oj9zFDTEnvrnw-Cyxpl33xcJcc,6006
|
13
|
+
provider/discovery/service.py,sha256=qU_fa8znPDNk-fQZ6Z3x6HDTbopK6RgfC_D8rixqBmY,709
|
14
|
+
provider/main.py,sha256=rC9WK3adULZ2hAQHdMgubBe-s23TXJuJnZrNySiJZSA,6081
|
15
|
+
provider/network/port_verifier.py,sha256=3l6WNwBHydggJRFYkAsuBp1eCxaU619kjWuM-zSVj2o,13267
|
16
|
+
provider/security/ethereum.py,sha256=EwPZj4JR8OEpto6LhKjuuT3Z9pBX6P7-UQaqJtqFkYQ,1242
|
17
|
+
provider/security/faucet.py,sha256=O2DgP3bIrRUm9tdLCdgnda9em0rPyeW42sWhO1EQJaA,5363
|
18
|
+
provider/service.py,sha256=Bc7jOemhqgz9adaWOFLbWum8HFY7fmf1Mmx50-Ygwuo,2389
|
19
|
+
provider/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
provider/utils/ascii_art.py,sha256=ykBFsztk57GIiz1NJ-EII5UvN74iECqQL4h9VmiW6Z8,3161
|
21
|
+
provider/utils/logging.py,sha256=VV3oTYSRT8hUejtXLuua1M6kCHmIJgPspIkzsUVhYW0,1920
|
22
|
+
provider/utils/port_display.py,sha256=u1HWQFA2kPbsM-TnsQfL6Hr4KmjIZWZfsjoxarHpbW0,11981
|
23
|
+
provider/utils/retry.py,sha256=1p12vZZkfotRDnv56jDXWG4W7jgGuZfRSXcbZC_ytTI,2935
|
24
|
+
provider/utils/setup.py,sha256=Z5dLuBQkb5vdoQsu1HJZwXmu9NWsiBYJ7Vq9-C-_tY8,2932
|
25
|
+
provider/vm/__init__.py,sha256=LJL504QGbqZvBbMN3G9ixMgAwvOWAKW37zUm_EiaW9M,508
|
26
|
+
provider/vm/cloud_init.py,sha256=E5dDH7dqStRcJNDfbarBBe83-c9N63W8B5ycIrHI8eU,4627
|
27
|
+
provider/vm/models.py,sha256=hNeXgOnXWyeSiYt07Pdks0B20cDi_VC8jV-tCxULNng,6350
|
28
|
+
provider/vm/multipass.py,sha256=rjO3GtuS4O_wXyYXSUiDGWYtQV2LpGxm6kITrA-ghBQ,617
|
29
|
+
provider/vm/multipass_adapter.py,sha256=1s3PtiaRmRxqOQJU651pIgZW1kwnZtl-Tclkk341TLM,10104
|
30
|
+
provider/vm/name_mapper.py,sha256=14nKfCjJ1WkXfC4vnCYIxNGQUwcl2vcxrJYUAz4fL40,4073
|
31
|
+
provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,12514
|
32
|
+
provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
|
33
|
+
provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
|
34
|
+
provider/vm/service.py,sha256=VpWhUuVtsmz9yUIiCT7XSoA0aA89I_4-g3JKbaGrryw,3651
|
35
|
+
golem_vm_provider-0.1.27.dist-info/METADATA,sha256=1CBJwjPlc1l3qSLH0eICzslk3nH0RbuaTV3cjgz8658,10793
|
36
|
+
golem_vm_provider-0.1.27.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
37
|
+
golem_vm_provider-0.1.27.dist-info/entry_points.txt,sha256=iFUajRxfViYkFcPMPv2EIjYm-BLOp86LJj_IjCwwZUw,74
|
38
|
+
golem_vm_provider-0.1.27.dist-info/RECORD,,
|
provider/api/models.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from pydantic import BaseModel, Field,
|
1
|
+
from pydantic import BaseModel, Field, field_validator
|
2
2
|
from typing import Dict, Optional, List, Any
|
3
3
|
from datetime import datetime
|
4
4
|
|
@@ -11,21 +11,21 @@ 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
|
+
@field_validator("name")
|
22
22
|
def validate_name(cls, v: str) -> str:
|
23
23
|
"""Validate VM name."""
|
24
24
|
if "--" in v:
|
25
25
|
raise ValueError("VM name cannot contain consecutive hyphens")
|
26
26
|
return v
|
27
27
|
|
28
|
-
@
|
28
|
+
@field_validator("resources", mode='before')
|
29
29
|
def validate_resources(cls, v: Optional[Dict[str, Any]], values: Dict[str, Any]) -> VMResources:
|
30
30
|
"""Validate and set resources."""
|
31
31
|
logger.debug(f"Validating resources input: {v}")
|
@@ -43,10 +43,10 @@ class CreateVMRequest(BaseModel):
|
|
43
43
|
return v
|
44
44
|
|
45
45
|
# If size provided, use that
|
46
|
-
if "size" in values and values["size"] is not None:
|
47
|
-
result = VMResources.from_size(values["size"])
|
46
|
+
if "size" in values.data and values.data["size"] is not None:
|
47
|
+
result = VMResources.from_size(values.data["size"])
|
48
48
|
logger.debug(
|
49
|
-
f"Created resources from size {values['size']}: {result}")
|
49
|
+
f"Created resources from size {values.data['size']}: {result}")
|
50
50
|
return result
|
51
51
|
|
52
52
|
# Only use defaults if nothing provided
|
@@ -57,7 +57,7 @@ class CreateVMRequest(BaseModel):
|
|
57
57
|
except Exception as e:
|
58
58
|
logger.error(f"Error validating resources: {e}")
|
59
59
|
logger.error(f"Input value: {v}")
|
60
|
-
logger.error(f"Values dict: {values}")
|
60
|
+
logger.error(f"Values dict: {values.data}")
|
61
61
|
raise ValueError(f"Invalid resource configuration: {str(e)}")
|
62
62
|
|
63
63
|
|
@@ -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):
|
provider/api/routes.py
CHANGED
@@ -3,150 +3,144 @@ from typing import List
|
|
3
3
|
from pathlib import Path
|
4
4
|
from fastapi import APIRouter, HTTPException, Request
|
5
5
|
|
6
|
-
from
|
7
|
-
from
|
6
|
+
from dependency_injector.wiring import inject, Provide
|
7
|
+
from fastapi import APIRouter, HTTPException, Depends
|
8
|
+
|
9
|
+
from ..config import Settings
|
10
|
+
from ..container import Container
|
11
|
+
from ..utils.logging import setup_logger
|
8
12
|
from ..utils.ascii_art import vm_creation_animation, vm_status_change
|
9
|
-
from ..vm.models import VMInfo,
|
13
|
+
from ..vm.models import VMInfo, VMAccessInfo, VMConfig, VMResources, VMNotFoundError
|
10
14
|
from .models import CreateVMRequest
|
11
|
-
from ..vm.
|
15
|
+
from ..vm.service import VMService
|
16
|
+
from ..vm.multipass_adapter import MultipassError
|
12
17
|
|
13
18
|
logger = setup_logger(__name__)
|
14
19
|
router = APIRouter()
|
15
20
|
|
21
|
+
|
16
22
|
@router.post("/vms", response_model=VMInfo)
|
17
|
-
|
23
|
+
@inject
|
24
|
+
async def create_vm(
|
25
|
+
request: CreateVMRequest,
|
26
|
+
vm_service: VMService = Depends(Provide[Container.vm_service]),
|
27
|
+
settings: Settings = Depends(Provide[Container.config]),
|
28
|
+
) -> VMInfo:
|
18
29
|
"""Create a new VM."""
|
19
30
|
try:
|
20
31
|
logger.info(f"📥 Received VM creation request for '{request.name}'")
|
21
32
|
|
22
|
-
|
23
|
-
resources = request.resources
|
24
|
-
if resources is None:
|
25
|
-
# This shouldn't happen due to validator, but just in case
|
26
|
-
resources = VMResources(cpu=1, memory=1, storage=10)
|
27
|
-
|
28
|
-
logger.info(f"📥 Using resources: {resources.cpu} CPU, {resources.memory}GB RAM, {resources.storage}GB storage")
|
33
|
+
resources = request.resources or VMResources()
|
29
34
|
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
if resources.storage < settings.MIN_STORAGE_GB:
|
38
|
-
logger.error(f"❌ Storage {resources.storage}GB below minimum {settings.MIN_STORAGE_GB}GB")
|
39
|
-
raise HTTPException(400, f"Minimum storage required: {settings.MIN_STORAGE_GB}GB")
|
40
|
-
|
41
|
-
# Check and allocate resources
|
42
|
-
logger.process("🔄 Allocating resources")
|
43
|
-
if not await req.app.state.resource_tracker.allocate(resources):
|
44
|
-
logger.error("❌ Insufficient resources available")
|
45
|
-
raise HTTPException(400, "Insufficient resources available on provider")
|
46
|
-
|
47
|
-
try:
|
48
|
-
# Create VM config
|
49
|
-
config = VMConfig(
|
50
|
-
name=request.name,
|
51
|
-
image=request.image or settings.DEFAULT_VM_IMAGE,
|
52
|
-
resources=resources,
|
53
|
-
ssh_key=request.ssh_key
|
54
|
-
)
|
55
|
-
|
56
|
-
# Create VM
|
57
|
-
logger.process(f"🔄 Creating VM with config: {config}")
|
58
|
-
vm_info = await req.app.state.provider.create_vm(config)
|
59
|
-
|
60
|
-
# Show success message
|
61
|
-
await vm_creation_animation(request.name)
|
62
|
-
return vm_info
|
63
|
-
except Exception as e:
|
64
|
-
# If VM creation fails, deallocate resources
|
65
|
-
logger.warning("⚠️ VM creation failed, deallocating resources")
|
66
|
-
await req.app.state.resource_tracker.deallocate(resources)
|
67
|
-
raise
|
35
|
+
# Create VM config
|
36
|
+
config = VMConfig(
|
37
|
+
name=request.name,
|
38
|
+
image=request.image or settings["DEFAULT_VM_IMAGE"],
|
39
|
+
resources=resources,
|
40
|
+
ssh_key=request.ssh_key
|
41
|
+
)
|
68
42
|
|
43
|
+
vm_info = await vm_service.create_vm(config)
|
44
|
+
await vm_creation_animation(request.name)
|
45
|
+
return vm_info
|
69
46
|
except MultipassError as e:
|
70
47
|
logger.error(f"Failed to create VM: {e}")
|
71
|
-
raise HTTPException(500, str(e))
|
48
|
+
raise HTTPException(status_code=500, detail=str(e))
|
49
|
+
except Exception as e:
|
50
|
+
logger.error(f"An unexpected error occurred: {e}")
|
51
|
+
raise HTTPException(status_code=500, detail="An unexpected error occurred")
|
52
|
+
|
72
53
|
|
73
54
|
@router.get("/vms", response_model=List[VMInfo])
|
74
|
-
|
55
|
+
@inject
|
56
|
+
async def list_vms(
|
57
|
+
vm_service: VMService = Depends(Provide[Container.vm_service]),
|
58
|
+
) -> List[VMInfo]:
|
75
59
|
"""List all VMs."""
|
76
60
|
try:
|
77
61
|
logger.info("📋 Listing all VMs")
|
78
|
-
|
79
|
-
for vm_id in req.app.state.resource_tracker.get_allocated_vms():
|
80
|
-
vm_info = await req.app.state.provider.get_vm_status(vm_id)
|
81
|
-
vms.append(vm_info)
|
82
|
-
return vms
|
62
|
+
return await vm_service.list_vms()
|
83
63
|
except MultipassError as e:
|
84
64
|
logger.error(f"Failed to list VMs: {e}")
|
85
|
-
raise HTTPException(500, str(e))
|
65
|
+
raise HTTPException(status_code=500, detail=str(e))
|
66
|
+
except Exception as e:
|
67
|
+
logger.error(f"An unexpected error occurred: {e}")
|
68
|
+
raise HTTPException(status_code=500, detail="An unexpected error occurred")
|
69
|
+
|
86
70
|
|
87
71
|
@router.get("/vms/{requestor_name}", response_model=VMInfo)
|
88
|
-
|
72
|
+
@inject
|
73
|
+
async def get_vm_status(
|
74
|
+
requestor_name: str,
|
75
|
+
vm_service: VMService = Depends(Provide[Container.vm_service]),
|
76
|
+
) -> VMInfo:
|
89
77
|
"""Get VM status."""
|
90
78
|
try:
|
91
79
|
logger.info(f"🔍 Getting status for VM '{requestor_name}'")
|
92
|
-
status = await
|
80
|
+
status = await vm_service.get_vm_status(requestor_name)
|
93
81
|
vm_status_change(requestor_name, status.status.value)
|
94
82
|
return status
|
83
|
+
except VMNotFoundError as e:
|
84
|
+
logger.error(f"VM not found: {e}")
|
85
|
+
raise HTTPException(status_code=404, detail=str(e))
|
95
86
|
except MultipassError as e:
|
96
87
|
logger.error(f"Failed to get VM status: {e}")
|
97
|
-
raise HTTPException(500, str(e))
|
88
|
+
raise HTTPException(status_code=500, detail=str(e))
|
89
|
+
except Exception as e:
|
90
|
+
logger.error(f"An unexpected error occurred: {e}")
|
91
|
+
raise HTTPException(status_code=500, detail="An unexpected error occurred")
|
92
|
+
|
98
93
|
|
99
94
|
@router.get("/vms/{requestor_name}/access", response_model=VMAccessInfo)
|
100
|
-
|
95
|
+
@inject
|
96
|
+
async def get_vm_access(
|
97
|
+
requestor_name: str,
|
98
|
+
vm_service: VMService = Depends(Provide[Container.vm_service]),
|
99
|
+
settings: Settings = Depends(Provide[Container.config]),
|
100
|
+
) -> VMAccessInfo:
|
101
101
|
"""Get VM access information."""
|
102
102
|
try:
|
103
|
-
|
104
|
-
vm = await req.app.state.provider.get_vm_status(requestor_name)
|
103
|
+
vm = await vm_service.get_vm_status(requestor_name)
|
105
104
|
if not vm:
|
106
105
|
raise HTTPException(404, "VM not found")
|
107
106
|
|
108
|
-
|
109
|
-
multipass_name = await req.app.state.provider.name_mapper.get_multipass_name(requestor_name)
|
107
|
+
multipass_name = await vm_service.name_mapper.get_multipass_name(requestor_name)
|
110
108
|
if not multipass_name:
|
111
109
|
raise HTTPException(404, "VM mapping not found")
|
112
110
|
|
113
|
-
# Return access info with both names
|
114
111
|
return VMAccessInfo(
|
115
|
-
ssh_host=settings
|
116
|
-
ssh_port=vm.ssh_port
|
112
|
+
ssh_host=settings["PUBLIC_IP"] or "localhost",
|
113
|
+
ssh_port=vm.ssh_port,
|
117
114
|
vm_id=requestor_name,
|
118
115
|
multipass_name=multipass_name
|
119
116
|
)
|
120
|
-
|
121
117
|
except MultipassError as e:
|
122
118
|
logger.error(f"Failed to get VM access info: {e}")
|
123
|
-
raise HTTPException(500, str(e))
|
119
|
+
raise HTTPException(status_code=500, detail=str(e))
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(f"An unexpected error occurred: {e}")
|
122
|
+
raise HTTPException(status_code=500, detail="An unexpected error occurred")
|
123
|
+
|
124
124
|
|
125
125
|
@router.delete("/vms/{requestor_name}")
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
"""
|
126
|
+
@inject
|
127
|
+
async def delete_vm(
|
128
|
+
requestor_name: str,
|
129
|
+
vm_service: VMService = Depends(Provide[Container.vm_service]),
|
130
|
+
) -> None:
|
131
|
+
"""Delete a VM."""
|
132
132
|
try:
|
133
133
|
logger.process(f"🗑️ Deleting VM '{requestor_name}'")
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
vm_status_change(requestor_name, "STOPPING", "Cleanup in progress")
|
143
|
-
await req.app.state.provider.delete_vm(requestor_name)
|
144
|
-
vm_status_change(requestor_name, "TERMINATED", "Cleanup complete")
|
145
|
-
logger.success(f"✨ Successfully deleted VM '{requestor_name}'")
|
146
|
-
except MultipassError as e:
|
147
|
-
logger.error(f"Failed to delete VM: {e}")
|
148
|
-
raise HTTPException(500, str(e))
|
149
|
-
|
150
|
-
except Exception as e:
|
134
|
+
vm_status_change(requestor_name, "STOPPING", "Cleanup in progress")
|
135
|
+
await vm_service.delete_vm(requestor_name)
|
136
|
+
vm_status_change(requestor_name, "TERMINATED", "Cleanup complete")
|
137
|
+
logger.success(f"✨ Successfully deleted VM '{requestor_name}'")
|
138
|
+
except VMNotFoundError as e:
|
139
|
+
logger.error(f"VM not found: {e}")
|
140
|
+
raise HTTPException(status_code=404, detail=str(e))
|
141
|
+
except MultipassError as e:
|
151
142
|
logger.error(f"Failed to delete VM: {e}")
|
152
|
-
raise HTTPException(500, str(e))
|
143
|
+
raise HTTPException(status_code=500, detail=str(e))
|
144
|
+
except Exception as e:
|
145
|
+
logger.error(f"An unexpected error occurred: {e}")
|
146
|
+
raise HTTPException(status_code=500, detail="An unexpected error occurred")
|
provider/config.py
CHANGED
@@ -2,8 +2,10 @@ import os
|
|
2
2
|
from pathlib import Path
|
3
3
|
from typing import Optional
|
4
4
|
import uuid
|
5
|
+
import socket
|
5
6
|
|
6
|
-
from
|
7
|
+
from pydantic_settings import BaseSettings
|
8
|
+
from pydantic import field_validator, Field
|
7
9
|
from .utils.logging import setup_logger
|
8
10
|
|
9
11
|
logger = setup_logger(__name__)
|
@@ -16,14 +18,29 @@ class Settings(BaseSettings):
|
|
16
18
|
DEBUG: bool = True
|
17
19
|
HOST: str = "0.0.0.0"
|
18
20
|
PORT: int = 7466
|
21
|
+
SKIP_PORT_VERIFICATION: bool = False
|
22
|
+
ENVIRONMENT: str = "production"
|
23
|
+
|
24
|
+
@property
|
25
|
+
def DEV_MODE(self) -> bool:
|
26
|
+
return self.ENVIRONMENT == "development"
|
27
|
+
|
28
|
+
@field_validator("SKIP_PORT_VERIFICATION", mode='before')
|
29
|
+
def set_skip_verification(cls, v: bool, values: dict) -> bool:
|
30
|
+
"""Set skip verification based on debug mode."""
|
31
|
+
return v or values.data.get("DEBUG", False)
|
19
32
|
|
20
33
|
# Provider Settings
|
21
|
-
PROVIDER_ID: str = "" # Will be set from Ethereum identity
|
22
34
|
PROVIDER_NAME: str = "golem-provider"
|
23
35
|
PROVIDER_COUNTRY: str = "SE"
|
24
36
|
ETHEREUM_KEY_DIR: str = ""
|
25
|
-
|
26
|
-
|
37
|
+
ETHEREUM_PRIVATE_KEY: Optional[str] = None
|
38
|
+
PROVIDER_ID: str = "" # Will be set from Ethereum identity
|
39
|
+
FAUCET_URL: str = "https://ethwarsaw.holesky.golemdb.io/faucet"
|
40
|
+
CAPTCHA_URL: str = "https://cap.gobas.me"
|
41
|
+
CAPTCHA_API_KEY: str = "05381a2cef5e"
|
42
|
+
|
43
|
+
@field_validator("ETHEREUM_KEY_DIR", mode='before')
|
27
44
|
def resolve_key_dir(cls, v: str) -> str:
|
28
45
|
"""Resolve Ethereum key directory path."""
|
29
46
|
if not v:
|
@@ -33,24 +50,56 @@ class Settings(BaseSettings):
|
|
33
50
|
path = Path.home() / path
|
34
51
|
return str(path)
|
35
52
|
|
36
|
-
@
|
37
|
-
def
|
38
|
-
"""Get
|
53
|
+
@field_validator("ETHEREUM_PRIVATE_KEY", mode='before')
|
54
|
+
def get_private_key(cls, v: Optional[str], values: dict) -> str:
|
55
|
+
"""Get private key from key file if not provided."""
|
39
56
|
from provider.security.ethereum import EthereumIdentity
|
40
57
|
|
41
|
-
# If ID provided in env, use it
|
42
58
|
if v:
|
43
59
|
return v
|
44
|
-
|
45
|
-
|
46
|
-
key_dir = values.get("ETHEREUM_KEY_DIR")
|
60
|
+
|
61
|
+
key_dir = values.data.get("ETHEREUM_KEY_DIR")
|
47
62
|
identity = EthereumIdentity(key_dir)
|
48
|
-
|
49
|
-
|
63
|
+
_, private_key = identity.get_or_create_identity()
|
64
|
+
return private_key
|
65
|
+
|
66
|
+
@field_validator("PROVIDER_ID", mode='before')
|
67
|
+
def get_provider_id(cls, v: str, values: dict) -> str:
|
68
|
+
"""Get provider ID from private key."""
|
69
|
+
from eth_account import Account
|
70
|
+
|
71
|
+
private_key = values.data.get("ETHEREUM_PRIVATE_KEY")
|
72
|
+
if not private_key:
|
73
|
+
raise ValueError("ETHEREUM_PRIVATE_KEY is not set")
|
74
|
+
|
75
|
+
acct = Account.from_key(private_key)
|
76
|
+
provider_id_from_key = acct.address
|
77
|
+
|
78
|
+
# If ID was provided via env, warn if it doesn't match
|
79
|
+
if v and v != provider_id_from_key:
|
80
|
+
logger.warning(
|
81
|
+
f"Provider ID from env ('{v}') does not match ID from key file ('{provider_id_from_key}'). "
|
82
|
+
"Using ID from key file."
|
83
|
+
)
|
84
|
+
|
85
|
+
return provider_id_from_key
|
86
|
+
|
87
|
+
@field_validator("PROVIDER_NAME", mode='before')
|
88
|
+
def set_provider_name(cls, v: str, values: dict) -> str:
|
89
|
+
"""Prefix provider name with DEVMODE if in development."""
|
90
|
+
if values.data.get("ENVIRONMENT") == "development":
|
91
|
+
return f"DEVMODE-{v}"
|
92
|
+
return v
|
93
|
+
|
50
94
|
# Discovery Service Settings
|
51
95
|
DISCOVERY_URL: str = "http://195.201.39.101:9001"
|
96
|
+
ADVERTISER_TYPE: str = "golem_base" # or "discovery_server"
|
52
97
|
ADVERTISEMENT_INTERVAL: int = 240 # seconds
|
53
98
|
|
99
|
+
# Golem Base Settings
|
100
|
+
GOLEM_BASE_RPC_URL: str = "https://ethwarsaw.holesky.golemdb.io/rpc"
|
101
|
+
GOLEM_BASE_WS_URL: str = "wss://ethwarsaw.holesky.golemdb.io/rpc/ws"
|
102
|
+
|
54
103
|
# VM Settings
|
55
104
|
MAX_VMS: int = 10
|
56
105
|
DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
|
@@ -59,7 +108,7 @@ class Settings(BaseSettings):
|
|
59
108
|
CLOUD_INIT_DIR: str = ""
|
60
109
|
CLOUD_INIT_FALLBACK_DIR: str = "" # Will be set to a temp directory if needed
|
61
110
|
|
62
|
-
@
|
111
|
+
@field_validator("CLOUD_INIT_DIR", mode='before')
|
63
112
|
def resolve_cloud_init_dir(cls, v: str) -> str:
|
64
113
|
"""Resolve and create cloud-init directory path."""
|
65
114
|
import platform
|
@@ -142,7 +191,7 @@ class Settings(BaseSettings):
|
|
142
191
|
logger.error(f"Failed to create cloud-init directory at {path}: {e}")
|
143
192
|
raise ValueError(f"Failed to create cloud-init directory: {e}")
|
144
193
|
|
145
|
-
@
|
194
|
+
@field_validator("VM_DATA_DIR", mode='before')
|
146
195
|
def resolve_vm_data_dir(cls, v: str) -> str:
|
147
196
|
"""Resolve and create VM data directory path."""
|
148
197
|
if not v:
|
@@ -161,7 +210,7 @@ class Settings(BaseSettings):
|
|
161
210
|
|
162
211
|
return str(path)
|
163
212
|
|
164
|
-
@
|
213
|
+
@field_validator("SSH_KEY_DIR", mode='before')
|
165
214
|
def resolve_ssh_key_dir(cls, v: str) -> str:
|
166
215
|
"""Resolve and create SSH key directory path with secure permissions."""
|
167
216
|
if not v:
|
@@ -200,7 +249,7 @@ class Settings(BaseSettings):
|
|
200
249
|
description="Path to multipass binary"
|
201
250
|
)
|
202
251
|
|
203
|
-
@
|
252
|
+
@field_validator("MULTIPASS_BINARY_PATH")
|
204
253
|
def detect_multipass_path(cls, v: str) -> str:
|
205
254
|
"""Detect and validate Multipass binary path."""
|
206
255
|
import platform
|
@@ -325,7 +374,7 @@ class Settings(BaseSettings):
|
|
325
374
|
PROXY_STATE_DIR: str = ""
|
326
375
|
PUBLIC_IP: Optional[str] = None
|
327
376
|
|
328
|
-
@
|
377
|
+
@field_validator("PROXY_STATE_DIR", mode='before')
|
329
378
|
def resolve_proxy_state_dir(cls, v: str) -> str:
|
330
379
|
"""Resolve and create proxy state directory path."""
|
331
380
|
if not v:
|
@@ -344,16 +393,47 @@ class Settings(BaseSettings):
|
|
344
393
|
|
345
394
|
return str(path)
|
346
395
|
|
347
|
-
@
|
348
|
-
def get_public_ip(cls, v: Optional[str]) -> Optional[str]:
|
396
|
+
@field_validator("PUBLIC_IP", mode='before')
|
397
|
+
def get_public_ip(cls, v: Optional[str], values: dict) -> Optional[str]:
|
349
398
|
"""Get public IP if set to 'auto'."""
|
399
|
+
if values.data.get("ENVIRONMENT") == "development":
|
400
|
+
try:
|
401
|
+
hostname = socket.gethostname()
|
402
|
+
ips = socket.gethostbyname_ex(hostname)[2]
|
403
|
+
local_ips = [ip for ip in ips if not ip.startswith("127.")]
|
404
|
+
if local_ips:
|
405
|
+
ip = local_ips[0]
|
406
|
+
logger.info(f"Found local IP for development: {ip}")
|
407
|
+
return ip
|
408
|
+
except socket.gaierror:
|
409
|
+
pass
|
410
|
+
|
411
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
412
|
+
try:
|
413
|
+
s.connect(('8.8.8.8', 80))
|
414
|
+
IP = s.getsockname()[0]
|
415
|
+
if IP:
|
416
|
+
logger.info(f"Found local IP for development: {IP}")
|
417
|
+
return IP
|
418
|
+
except Exception:
|
419
|
+
pass
|
420
|
+
finally:
|
421
|
+
s.close()
|
422
|
+
|
423
|
+
raise ValueError("Could not determine local IP address in development mode. "
|
424
|
+
"Please ensure you have a valid network connection.")
|
350
425
|
if v == "auto":
|
351
426
|
try:
|
352
427
|
import requests
|
353
428
|
response = requests.get("https://api.ipify.org")
|
354
|
-
|
429
|
+
ip = response.text.strip()
|
430
|
+
logger.info(f"Found public IP: {ip}")
|
431
|
+
return ip
|
355
432
|
except Exception:
|
356
433
|
return None
|
434
|
+
|
435
|
+
if v:
|
436
|
+
logger.info(f"Using manually provided IP: {v}")
|
357
437
|
return v
|
358
438
|
|
359
439
|
class Config:
|