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.
@@ -1,29 +1,30 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.24
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.9,<4.0
8
+ Requires-Python: >=3.13,<4.0
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Environment :: Console
11
11
  Classifier: Intended Audience :: System Administrators
12
12
  Classifier: Operating System :: POSIX :: Linux
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
14
  Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.9
19
16
  Classifier: Topic :: System :: Distributed Computing
20
17
  Requires-Dist: aiohttp (>=3.8.1,<4.0.0)
21
18
  Requires-Dist: colorlog (>=6.8.0,<7.0.0)
22
19
  Requires-Dist: cryptography (>=3.4.7,<4.0.0)
23
- Requires-Dist: eth-account (>=0.8.0,<0.9.0)
24
- Requires-Dist: fastapi (>=0.68.0,<0.69.0)
20
+ Requires-Dist: 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 (>=1.8.2,<2.0.0)
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
- poetry run python run.py
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ dev=provider.main:dev
3
+ golem-provider=provider.main:cli
4
+
provider/api/models.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel, Field, validator
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
- regex="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
14
+ pattern="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
15
15
  size: Optional[VMSize] = None
16
16
  resources: Optional[VMResources] = None
17
17
  image: str = Field(default="24.04") # Ubuntu 24.04 LTS
18
- ssh_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ",
18
+ ssh_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ",
19
19
  description="SSH public key for VM access")
20
20
 
21
- @validator("name")
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
- @validator("resources", pre=True)
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(..., regex="^(ssh-rsa|ssh-ed25519) ")
85
+ public_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ")
86
86
 
87
87
 
88
88
  class ErrorResponse(BaseModel):
provider/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 ..config import settings
7
- from ..utils.logging import setup_logger, PROCESS, SUCCESS
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, VMStatus, VMAccessInfo, VMConfig, VMResources
13
+ from ..vm.models import VMInfo, VMAccessInfo, VMConfig, VMResources, VMNotFoundError
10
14
  from .models import CreateVMRequest
11
- from ..vm.multipass import MultipassProvider, MultipassError
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
- async def create_vm(request: CreateVMRequest, req: Request) -> VMInfo:
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
- # Determine resources based on request
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
- # Validate against minimum requirements
31
- if resources.cpu < settings.MIN_CPU_CORES:
32
- logger.error(f"❌ CPU cores {resources.cpu} below minimum {settings.MIN_CPU_CORES}")
33
- raise HTTPException(400, f"Minimum CPU cores required: {settings.MIN_CPU_CORES}")
34
- if resources.memory < settings.MIN_MEMORY_GB:
35
- logger.error(f"❌ Memory {resources.memory}GB below minimum {settings.MIN_MEMORY_GB}GB")
36
- raise HTTPException(400, f"Minimum memory required: {settings.MIN_MEMORY_GB}GB")
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
- async def list_vms(req: Request) -> List[VMInfo]:
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
- vms = []
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
- async def get_vm_status(requestor_name: str, req: Request) -> VMInfo:
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 req.app.state.provider.get_vm_status(requestor_name)
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
- async def get_vm_access(requestor_name: str, req: Request) -> VMAccessInfo:
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
- # Get VM info
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
- # Get multipass name from mapper
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.PUBLIC_IP or "localhost",
116
- ssh_port=vm.ssh_port or 22,
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
- async def delete_vm(requestor_name: str, req: Request) -> None:
127
- """Delete a VM.
128
-
129
- Args:
130
- requestor_name: Name of the VM as provided by requestor
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
- # Get multipass name from mapper
136
- multipass_name = await req.app.state.provider.name_mapper.get_multipass_name(requestor_name)
137
- if not multipass_name:
138
- logger.warning(f"No multipass name found for VM '{requestor_name}' (may have been already deleted)")
139
- return
140
-
141
- try:
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 pydantic import BaseSettings, validator, Field
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
- @validator("ETHEREUM_KEY_DIR", pre=True)
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
- @validator("PROVIDER_ID", always=True)
37
- def get_or_create_provider_id(cls, v: str, values: dict) -> str:
38
- """Get or create provider ID from Ethereum identity."""
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
- # Get ID from Ethereum identity
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
- return identity.get_or_create_identity()
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
- @validator("CLOUD_INIT_DIR", pre=True)
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
- @validator("VM_DATA_DIR", pre=True)
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
- @validator("SSH_KEY_DIR", pre=True)
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
- @validator("MULTIPASS_BINARY_PATH")
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
- @validator("PROXY_STATE_DIR", pre=True)
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
- @validator("PUBLIC_IP", pre=True)
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
- return response.text.strip()
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: