golem-vm-provider 0.1.12__tar.gz → 0.1.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/PKG-INFO +1 -1
  2. golem_vm_provider-0.1.14/provider/config.py +280 -0
  3. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/main.py +4 -32
  4. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/pyproject.toml +1 -1
  5. golem_vm_provider-0.1.12/provider/config.py +0 -218
  6. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/README.md +0 -0
  7. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/__init__.py +0 -0
  8. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/api/__init__.py +0 -0
  9. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/api/models.py +0 -0
  10. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/api/routes.py +0 -0
  11. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/discovery/__init__.py +0 -0
  12. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/discovery/advertiser.py +0 -0
  13. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/discovery/resource_tracker.py +0 -0
  14. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/network/port_verifier.py +0 -0
  15. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/security/ethereum.py +0 -0
  16. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/utils/ascii_art.py +0 -0
  17. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/utils/logging.py +0 -0
  18. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/utils/port_display.py +0 -0
  19. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/utils/retry.py +0 -0
  20. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/vm/__init__.py +0 -0
  21. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/vm/cloud_init.py +0 -0
  22. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/vm/models.py +0 -0
  23. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/vm/multipass.py +0 -0
  24. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/vm/name_mapper.py +0 -0
  25. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/vm/port_manager.py +0 -0
  26. {golem_vm_provider-0.1.12 → golem_vm_provider-0.1.14}/provider/vm/proxy_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: golem-vm-provider
3
- Version: 0.1.12
3
+ Version: 0.1.14
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
@@ -0,0 +1,280 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional
4
+ import uuid
5
+
6
+ from pydantic import BaseSettings, validator, Field
7
+ from .utils.logging import setup_logger
8
+
9
+ logger = setup_logger(__name__)
10
+
11
+
12
+ class Settings(BaseSettings):
13
+ """Provider configuration settings."""
14
+
15
+ # API Settings
16
+ DEBUG: bool = True
17
+ HOST: str = "0.0.0.0"
18
+ PORT: int = 7466
19
+
20
+ # Provider Settings
21
+ PROVIDER_ID: str = "" # Will be set from Ethereum identity
22
+ PROVIDER_NAME: str = "golem-provider"
23
+ PROVIDER_COUNTRY: str = "SE"
24
+ ETHEREUM_KEY_DIR: str = ""
25
+
26
+ @validator("ETHEREUM_KEY_DIR", pre=True)
27
+ def resolve_key_dir(cls, v: str) -> str:
28
+ """Resolve Ethereum key directory path."""
29
+ if not v:
30
+ return str(Path.home() / ".golem" / "provider" / "keys")
31
+ path = Path(v)
32
+ if not path.is_absolute():
33
+ path = Path.home() / path
34
+ return str(path)
35
+
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."""
39
+ from provider.security.ethereum import EthereumIdentity
40
+
41
+ # If ID provided in env, use it
42
+ if v:
43
+ return v
44
+
45
+ # Get ID from Ethereum identity
46
+ key_dir = values.get("ETHEREUM_KEY_DIR")
47
+ identity = EthereumIdentity(key_dir)
48
+ return identity.get_or_create_identity()
49
+
50
+ # Discovery Service Settings
51
+ DISCOVERY_URL: str = "http://195.201.39.101:9001"
52
+ ADVERTISEMENT_INTERVAL: int = 240 # seconds
53
+
54
+ # VM Settings
55
+ MAX_VMS: int = 10
56
+ DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
57
+ VM_DATA_DIR: str = ""
58
+ SSH_KEY_DIR: str = ""
59
+
60
+ @validator("VM_DATA_DIR", pre=True)
61
+ def resolve_vm_data_dir(cls, v: str) -> str:
62
+ """Resolve and create VM data directory path."""
63
+ if not v:
64
+ path = Path.home() / ".golem" / "provider" / "vms"
65
+ else:
66
+ path = Path(v)
67
+ if not path.is_absolute():
68
+ path = Path.home() / path
69
+
70
+ try:
71
+ path.mkdir(parents=True, exist_ok=True)
72
+ logger.debug(f"Created VM data directory at {path}")
73
+ except Exception as e:
74
+ logger.error(f"Failed to create VM data directory at {path}: {e}")
75
+ raise ValueError(f"Failed to create VM data directory: {e}")
76
+
77
+ return str(path)
78
+
79
+ @validator("SSH_KEY_DIR", pre=True)
80
+ def resolve_ssh_key_dir(cls, v: str) -> str:
81
+ """Resolve and create SSH key directory path with secure permissions."""
82
+ if not v:
83
+ path = Path.home() / ".golem" / "provider" / "ssh"
84
+ else:
85
+ path = Path(v)
86
+ if not path.is_absolute():
87
+ path = Path.home() / path
88
+
89
+ try:
90
+ path.mkdir(parents=True, exist_ok=True)
91
+ path.chmod(0o700) # Secure permissions for SSH keys
92
+ logger.debug(f"Created SSH key directory at {path} with secure permissions")
93
+ except Exception as e:
94
+ logger.error(f"Failed to create SSH key directory at {path}: {e}")
95
+ raise ValueError(f"Failed to create SSH key directory: {e}")
96
+
97
+ return str(path)
98
+
99
+ # Resource Settings
100
+ MIN_MEMORY_GB: int = 1
101
+ MIN_STORAGE_GB: int = 10
102
+ MIN_CPU_CORES: int = 1
103
+
104
+ # Resource Thresholds (%)
105
+ CPU_THRESHOLD: int = 90
106
+ MEMORY_THRESHOLD: int = 85
107
+ STORAGE_THRESHOLD: int = 90
108
+
109
+ # Rate Limiting
110
+ RATE_LIMIT_PER_MINUTE: int = 100
111
+
112
+ # Multipass Settings
113
+ MULTIPASS_BINARY_PATH: str = Field(
114
+ default="",
115
+ description="Path to multipass binary"
116
+ )
117
+
118
+ @validator("MULTIPASS_BINARY_PATH")
119
+ def detect_multipass_path(cls, v: str) -> str:
120
+ """Detect and validate Multipass binary path."""
121
+ import platform
122
+ import subprocess
123
+
124
+ def validate_path(path: str) -> bool:
125
+ """Validate that a path exists and is executable."""
126
+ return os.path.isfile(path) and os.access(path, os.X_OK)
127
+
128
+ # If path provided via environment variable, ONLY validate that path
129
+ if v:
130
+ logger.info(f"Checking multipass binary at: {v}")
131
+ if not validate_path(v):
132
+ msg = f"Invalid multipass binary path: {v} (not found or not executable)"
133
+ logger.error(msg)
134
+ raise ValueError(msg)
135
+ logger.info(f"✓ Found valid multipass binary at: {v}")
136
+ return v
137
+
138
+ logger.info("No multipass path provided, attempting auto-detection...")
139
+ system = platform.system().lower()
140
+ logger.info(f"Detected OS: {system}")
141
+ binary_name = "multipass.exe" if system == "windows" else "multipass"
142
+
143
+ # Try to find multipass based on OS
144
+ if system == "linux":
145
+ logger.info("Checking for snap installation...")
146
+ # First try to find snap and check if multipass is installed
147
+ try:
148
+ # Check if snap exists
149
+ snap_result = subprocess.run(
150
+ ["which", "snap"],
151
+ capture_output=True,
152
+ text=True,
153
+ check=True
154
+ )
155
+ if snap_result.returncode == 0:
156
+ logger.info("✓ Found snap, checking for multipass installation...")
157
+ # Check if multipass is installed via snap
158
+ try:
159
+ snap_list = subprocess.run(
160
+ ["snap", "list", "multipass"],
161
+ capture_output=True,
162
+ text=True,
163
+ check=True
164
+ )
165
+ if snap_list.returncode == 0:
166
+ snap_path = "/snap/bin/multipass"
167
+ if validate_path(snap_path):
168
+ logger.info(f"✓ Found multipass via snap at {snap_path}")
169
+ return snap_path
170
+ except subprocess.CalledProcessError:
171
+ logger.info("✗ Multipass not installed via snap")
172
+ pass
173
+ except subprocess.CalledProcessError:
174
+ logger.info("✗ Snap not found")
175
+ pass
176
+
177
+ # Common Linux paths if snap installation not found
178
+ search_paths = [
179
+ "/usr/local/bin",
180
+ "/usr/bin",
181
+ "/snap/bin"
182
+ ]
183
+ logger.info(f"Checking common Linux paths: {', '.join(search_paths)}")
184
+
185
+ elif system == "darwin": # macOS
186
+ search_paths = [
187
+ "/opt/homebrew/bin", # M1 Mac
188
+ "/usr/local/bin", # Intel Mac
189
+ "/opt/local/bin" # MacPorts
190
+ ]
191
+ logger.info(f"Checking macOS paths: {', '.join(search_paths)}")
192
+
193
+ elif system == "windows":
194
+ search_paths = [
195
+ os.path.expandvars(r"%ProgramFiles%\Multipass"),
196
+ os.path.expandvars(r"%ProgramFiles(x86)%\Multipass"),
197
+ os.path.expandvars(r"%LocalAppData%\Multipass")
198
+ ]
199
+ logger.info(f"Checking Windows paths: {', '.join(search_paths)}")
200
+
201
+ else:
202
+ search_paths = ["/usr/local/bin", "/usr/bin"]
203
+ logger.info(f"Checking default paths: {', '.join(search_paths)}")
204
+
205
+ # Search for multipass binary in OS-specific paths
206
+ for directory in search_paths:
207
+ path = os.path.join(directory, binary_name)
208
+ if validate_path(path):
209
+ logger.info(f"✓ Found valid multipass binary at: {path}")
210
+ return path
211
+
212
+ # OS-specific installation instructions
213
+ if system == "linux":
214
+ raise ValueError(
215
+ "Multipass binary not found. Please install using:\n"
216
+ "sudo snap install multipass\n"
217
+ "Or set GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
218
+ )
219
+ elif system == "darwin":
220
+ raise ValueError(
221
+ "Multipass binary not found. Please install using:\n"
222
+ "brew install multipass\n"
223
+ "Or set GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
224
+ )
225
+ elif system == "windows":
226
+ raise ValueError(
227
+ "Multipass binary not found. Please install from:\n"
228
+ "Microsoft Store or https://multipass.run/download/windows\n"
229
+ "Or set GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
230
+ )
231
+ else:
232
+ raise ValueError(
233
+ "Multipass binary not found. Please install Multipass or set "
234
+ "GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
235
+ )
236
+
237
+ # Proxy Settings
238
+ PORT_RANGE_START: int = 50800
239
+ PORT_RANGE_END: int = 50900
240
+ PROXY_STATE_DIR: str = ""
241
+ PUBLIC_IP: Optional[str] = None
242
+
243
+ @validator("PROXY_STATE_DIR", pre=True)
244
+ def resolve_proxy_state_dir(cls, v: str) -> str:
245
+ """Resolve and create proxy state directory path."""
246
+ if not v:
247
+ path = Path.home() / ".golem" / "provider" / "proxy"
248
+ else:
249
+ path = Path(v)
250
+ if not path.is_absolute():
251
+ path = Path.home() / path
252
+
253
+ try:
254
+ path.mkdir(parents=True, exist_ok=True)
255
+ logger.debug(f"Created proxy state directory at {path}")
256
+ except Exception as e:
257
+ logger.error(f"Failed to create proxy state directory at {path}: {e}")
258
+ raise ValueError(f"Failed to create proxy state directory: {e}")
259
+
260
+ return str(path)
261
+
262
+ @validator("PUBLIC_IP", pre=True)
263
+ def get_public_ip(cls, v: Optional[str]) -> Optional[str]:
264
+ """Get public IP if set to 'auto'."""
265
+ if v == "auto":
266
+ try:
267
+ import requests
268
+ response = requests.get("https://api.ipify.org")
269
+ return response.text.strip()
270
+ except Exception:
271
+ return None
272
+ return v
273
+
274
+ class Config:
275
+ env_prefix = "GOLEM_PROVIDER_"
276
+ case_sensitive = True
277
+
278
+
279
+ # Global settings instance
280
+ settings = Settings()
@@ -163,41 +163,13 @@ __all__ = ["app", "start"]
163
163
 
164
164
  def check_requirements():
165
165
  """Check if all requirements are met."""
166
- import os
167
- from pathlib import Path
168
-
169
- # Check if multipass is installed
170
- multipass_path = os.environ.get('GOLEM_PROVIDER_MULTIPASS_BINARY_PATH', '/usr/local/bin/multipass')
171
- if not Path(multipass_path).exists():
172
- logger.error(f"Multipass binary not found at {multipass_path}")
173
- return False
174
-
175
- # Check required directories
176
- vm_data_dir = os.environ.get(
177
- 'GOLEM_PROVIDER_VM_DATA_DIR',
178
- str(Path.home() / '.golem' / 'provider' / 'vms')
179
- )
180
- ssh_key_dir = os.environ.get(
181
- 'GOLEM_PROVIDER_SSH_KEY_DIR',
182
- str(Path.home() / '.golem' / 'provider' / 'ssh')
183
- )
184
- proxy_state_dir = os.environ.get(
185
- 'GOLEM_PROVIDER_PROXY_STATE_DIR',
186
- str(Path.home() / '.golem' / 'provider' / 'proxy')
187
- )
188
-
189
166
  try:
190
- # Create and secure directories
191
- for directory in [vm_data_dir, ssh_key_dir, proxy_state_dir]:
192
- path = Path(directory)
193
- path.mkdir(parents=True, exist_ok=True)
194
- if directory == ssh_key_dir:
195
- path.chmod(0o700) # Secure permissions for SSH keys
167
+ # Import settings to trigger validation
168
+ from .config import settings
169
+ return True
196
170
  except Exception as e:
197
- logger.error(f"Failed to create required directories: {e}")
171
+ logger.error(f"Requirements check failed: {e}")
198
172
  return False
199
-
200
- return True
201
173
 
202
174
  async def verify_provider_port(port: int) -> bool:
203
175
  """Verify that the provider port is available for binding.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "golem-vm-provider"
3
- version = "0.1.12"
3
+ version = "0.1.14"
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"
@@ -1,218 +0,0 @@
1
- import os
2
- from pathlib import Path
3
- from typing import Optional
4
- import uuid
5
-
6
- from pydantic import BaseSettings, validator
7
-
8
-
9
- class Settings(BaseSettings):
10
- """Provider configuration settings."""
11
-
12
- # API Settings
13
- DEBUG: bool = True
14
- HOST: str = "0.0.0.0"
15
- PORT: int = 7466
16
-
17
- # Provider Settings
18
- PROVIDER_ID: str = "" # Will be set from Ethereum identity
19
- PROVIDER_NAME: str = "golem-provider"
20
- PROVIDER_COUNTRY: str = "SE"
21
- ETHEREUM_KEY_DIR: str = ""
22
-
23
- @validator("ETHEREUM_KEY_DIR", pre=True)
24
- def resolve_key_dir(cls, v: str) -> str:
25
- """Resolve Ethereum key directory path."""
26
- if not v:
27
- return str(Path.home() / ".golem" / "provider" / "keys")
28
- path = Path(v)
29
- if not path.is_absolute():
30
- path = Path.home() / path
31
- return str(path)
32
-
33
- @validator("PROVIDER_ID", always=True)
34
- def get_or_create_provider_id(cls, v: str, values: dict) -> str:
35
- """Get or create provider ID from Ethereum identity."""
36
- from provider.security.ethereum import EthereumIdentity
37
-
38
- # If ID provided in env, use it
39
- if v:
40
- return v
41
-
42
- # Get ID from Ethereum identity
43
- key_dir = values.get("ETHEREUM_KEY_DIR")
44
- identity = EthereumIdentity(key_dir)
45
- return identity.get_or_create_identity()
46
-
47
- # Discovery Service Settings
48
- DISCOVERY_URL: str = "http://195.201.39.101:9001"
49
- ADVERTISEMENT_INTERVAL: int = 240 # seconds
50
-
51
- # VM Settings
52
- MAX_VMS: int = 10
53
- DEFAULT_VM_IMAGE: str = "ubuntu:24.04"
54
- VM_DATA_DIR: str = ""
55
- SSH_KEY_DIR: str = ""
56
-
57
- @validator("VM_DATA_DIR", pre=True)
58
- def resolve_vm_data_dir(cls, v: str) -> str:
59
- """Resolve VM data directory path."""
60
- if not v:
61
- return str(Path.home() / ".golem" / "provider" / "vms")
62
- path = Path(v)
63
- if not path.is_absolute():
64
- path = Path.home() / path
65
- return str(path)
66
-
67
- @validator("SSH_KEY_DIR", pre=True)
68
- def resolve_ssh_key_dir(cls, v: str) -> str:
69
- """Resolve SSH key directory path."""
70
- if not v:
71
- return str(Path.home() / ".golem" / "provider" / "ssh")
72
- path = Path(v)
73
- if not path.is_absolute():
74
- path = Path.home() / path
75
- return str(path)
76
-
77
- # Resource Settings
78
- MIN_MEMORY_GB: int = 1
79
- MIN_STORAGE_GB: int = 10
80
- MIN_CPU_CORES: int = 1
81
-
82
- # Resource Thresholds (%)
83
- CPU_THRESHOLD: int = 90
84
- MEMORY_THRESHOLD: int = 85
85
- STORAGE_THRESHOLD: int = 90
86
-
87
- # Rate Limiting
88
- RATE_LIMIT_PER_MINUTE: int = 100
89
-
90
- # Multipass Settings
91
- MULTIPASS_BINARY_PATH: str = ""
92
-
93
- @validator("MULTIPASS_BINARY_PATH", pre=True)
94
- def detect_multipass_path(cls, v: str) -> str:
95
- """Detect and validate Multipass binary path."""
96
- if v:
97
- path = v
98
- else:
99
- import platform
100
- import subprocess
101
-
102
- system = platform.system().lower()
103
- binary_name = "multipass.exe" if system == "windows" else "multipass"
104
-
105
- # Try to find multipass based on OS
106
- if system == "linux":
107
- # First try to find snap
108
- try:
109
- snap_result = subprocess.run(
110
- ["which", "snap"],
111
- capture_output=True,
112
- text=True
113
- )
114
- if snap_result.returncode == 0:
115
- # If snap exists, check if multipass is installed
116
- snap_path = "/snap/bin/multipass"
117
- if os.path.isfile(snap_path) and os.access(snap_path, os.X_OK):
118
- return snap_path
119
- except subprocess.SubprocessError:
120
- pass
121
-
122
- # Common Linux paths
123
- search_paths = [
124
- "/usr/local/bin",
125
- "/usr/bin",
126
- "/snap/bin"
127
- ]
128
-
129
- elif system == "darwin": # macOS
130
- search_paths = [
131
- "/opt/homebrew/bin", # M1 Mac
132
- "/usr/local/bin", # Intel Mac
133
- "/opt/local/bin" # MacPorts
134
- ]
135
-
136
- elif system == "windows":
137
- search_paths = [
138
- os.path.expandvars(r"%ProgramFiles%\Multipass"),
139
- os.path.expandvars(r"%ProgramFiles(x86)%\Multipass"),
140
- os.path.expandvars(r"%LocalAppData%\Multipass")
141
- ]
142
-
143
- else:
144
- search_paths = ["/usr/local/bin", "/usr/bin"]
145
-
146
- # Search for multipass binary in OS-specific paths
147
- for directory in search_paths:
148
- path = os.path.join(directory, binary_name)
149
- if os.path.isfile(path) and os.access(path, os.X_OK):
150
- return path
151
-
152
- # OS-specific installation instructions
153
- if system == "linux":
154
- raise ValueError(
155
- "Multipass binary not found. Please install using:\n"
156
- "sudo snap install multipass\n"
157
- "Or set GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
158
- )
159
- elif system == "darwin":
160
- raise ValueError(
161
- "Multipass binary not found. Please install using:\n"
162
- "brew install multipass\n"
163
- "Or set GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
164
- )
165
- elif system == "windows":
166
- raise ValueError(
167
- "Multipass binary not found. Please install from:\n"
168
- "Microsoft Store or https://multipass.run/download/windows\n"
169
- "Or set GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
170
- )
171
- else:
172
- raise ValueError(
173
- "Multipass binary not found. Please install Multipass or set "
174
- "GOLEM_PROVIDER_MULTIPASS_BINARY_PATH to your Multipass binary path."
175
- )
176
-
177
- # Validate the path
178
- if not os.path.isfile(path):
179
- raise ValueError(f"Multipass binary not found at: {path}")
180
- if not os.access(path, os.X_OK):
181
- raise ValueError(f"Multipass binary at {path} is not executable")
182
- return path
183
-
184
- # Proxy Settings
185
- PORT_RANGE_START: int = 50800
186
- PORT_RANGE_END: int = 50900
187
- PROXY_STATE_DIR: str = ""
188
- PUBLIC_IP: Optional[str] = None
189
-
190
- @validator("PROXY_STATE_DIR", pre=True)
191
- def resolve_proxy_state_dir(cls, v: str) -> str:
192
- """Resolve proxy state directory path."""
193
- if not v:
194
- return str(Path.home() / ".golem" / "provider" / "proxy")
195
- path = Path(v)
196
- if not path.is_absolute():
197
- path = Path.home() / path
198
- return str(path)
199
-
200
- @validator("PUBLIC_IP", pre=True)
201
- def get_public_ip(cls, v: Optional[str]) -> Optional[str]:
202
- """Get public IP if set to 'auto'."""
203
- if v == "auto":
204
- try:
205
- import requests
206
- response = requests.get("https://api.ipify.org")
207
- return response.text.strip()
208
- except Exception:
209
- return None
210
- return v
211
-
212
- class Config:
213
- env_prefix = "GOLEM_PROVIDER_"
214
- case_sensitive = True
215
-
216
-
217
- # Global settings instance
218
- settings = Settings()