golem-vm-provider 0.1.0__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.0.dist-info/METADATA +398 -0
- golem_vm_provider-0.1.0.dist-info/RECORD +26 -0
- golem_vm_provider-0.1.0.dist-info/WHEEL +4 -0
- golem_vm_provider-0.1.0.dist-info/entry_points.txt +3 -0
- provider/__init__.py +3 -0
- provider/api/__init__.py +19 -0
- provider/api/models.py +108 -0
- provider/api/routes.py +159 -0
- provider/config.py +160 -0
- provider/discovery/__init__.py +6 -0
- provider/discovery/advertiser.py +179 -0
- provider/discovery/resource_tracker.py +152 -0
- provider/main.py +125 -0
- provider/network/port_verifier.py +287 -0
- provider/security/ethereum.py +41 -0
- provider/utils/ascii_art.py +79 -0
- provider/utils/logging.py +82 -0
- provider/utils/port_display.py +204 -0
- provider/utils/retry.py +48 -0
- provider/vm/__init__.py +31 -0
- provider/vm/cloud_init.py +67 -0
- provider/vm/models.py +205 -0
- provider/vm/multipass.py +427 -0
- provider/vm/name_mapper.py +108 -0
- provider/vm/port_manager.py +196 -0
- provider/vm/proxy_manager.py +239 -0
@@ -0,0 +1,196 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import asyncio
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Optional, Set, List, Dict
|
7
|
+
from threading import Lock
|
8
|
+
|
9
|
+
from ..config import settings
|
10
|
+
from ..network.port_verifier import PortVerifier, PortVerificationResult
|
11
|
+
from ..utils.port_display import PortVerificationDisplay
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
class PortManager:
|
16
|
+
"""Manages port allocation and verification for VM SSH proxying."""
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
start_port: int = 50800,
|
21
|
+
end_port: int = 50900,
|
22
|
+
state_file: Optional[str] = None,
|
23
|
+
port_check_servers: Optional[List[str]] = None,
|
24
|
+
discovery_port: Optional[int] = None
|
25
|
+
):
|
26
|
+
"""Initialize the port manager.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
start_port: Beginning of port range
|
30
|
+
end_port: End of port range (exclusive)
|
31
|
+
state_file: Path to persist port assignments
|
32
|
+
port_check_servers: List of URLs for port checking services
|
33
|
+
"""
|
34
|
+
self.start_port = start_port
|
35
|
+
self.end_port = end_port
|
36
|
+
self.state_file = state_file or os.path.expanduser("~/.golem/provider/ports.json")
|
37
|
+
self.lock = Lock()
|
38
|
+
self._used_ports: dict[str, int] = {} # vm_id -> port
|
39
|
+
self.verified_ports: Set[int] = set()
|
40
|
+
|
41
|
+
# Initialize port verifier with default servers
|
42
|
+
self.port_check_servers = port_check_servers or [
|
43
|
+
# "http://portcheck1.golem.network:7466",
|
44
|
+
# "http://portcheck2.golem.network:7466",
|
45
|
+
# "http://portcheck3.golem.network:7466",
|
46
|
+
"http://localhost:9000" # Fallback for local development
|
47
|
+
]
|
48
|
+
self.discovery_port = discovery_port or settings.PORT
|
49
|
+
self.port_verifier = PortVerifier(
|
50
|
+
self.port_check_servers,
|
51
|
+
discovery_port=self.discovery_port
|
52
|
+
)
|
53
|
+
|
54
|
+
self._load_state()
|
55
|
+
|
56
|
+
async def initialize(self) -> bool:
|
57
|
+
"""Initialize port manager with verification.
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
bool: True if required ports were verified successfully
|
61
|
+
"""
|
62
|
+
from ..config import settings
|
63
|
+
|
64
|
+
display = PortVerificationDisplay(
|
65
|
+
provider_port=self.discovery_port,
|
66
|
+
port_range_start=self.start_port,
|
67
|
+
port_range_end=self.end_port
|
68
|
+
)
|
69
|
+
display.print_header()
|
70
|
+
|
71
|
+
# Verify all ports together (discovery port and SSH ports)
|
72
|
+
all_ports = [self.discovery_port] + list(range(self.start_port, self.end_port))
|
73
|
+
logger.info(f"Verifying all ports: discovery port {self.discovery_port} and SSH ports {self.start_port}-{self.end_port}...")
|
74
|
+
|
75
|
+
results = await self.port_verifier.verify_ports(all_ports)
|
76
|
+
|
77
|
+
# Display discovery port status with animation
|
78
|
+
discovery_result = results[self.discovery_port]
|
79
|
+
await display.print_discovery_status(discovery_result)
|
80
|
+
|
81
|
+
if not discovery_result.accessible:
|
82
|
+
logger.error(f"Failed to verify discovery port: {discovery_result.error}")
|
83
|
+
# Print summary before returning
|
84
|
+
display.print_summary(discovery_result, {})
|
85
|
+
return False
|
86
|
+
|
87
|
+
# Display SSH ports status with animation
|
88
|
+
ssh_results = {port: result for port, result in results.items() if port != self.discovery_port}
|
89
|
+
await display.print_ssh_status(ssh_results)
|
90
|
+
|
91
|
+
# Store verified ports
|
92
|
+
self.verified_ports = {port for port, result in ssh_results.items() if result.accessible}
|
93
|
+
|
94
|
+
# Only show critical issues and quick fix if there are problems
|
95
|
+
if not discovery_result.accessible or not self.verified_ports:
|
96
|
+
display.print_critical_issues(discovery_result, ssh_results)
|
97
|
+
display.print_quick_fix(discovery_result, ssh_results)
|
98
|
+
|
99
|
+
# Print precise summary of current status
|
100
|
+
display.print_summary(discovery_result, ssh_results)
|
101
|
+
|
102
|
+
if not self.verified_ports:
|
103
|
+
logger.error("No SSH ports were verified as accessible")
|
104
|
+
return False
|
105
|
+
|
106
|
+
logger.info(f"Successfully verified {len(self.verified_ports)} SSH ports")
|
107
|
+
return True
|
108
|
+
|
109
|
+
def _load_state(self) -> None:
|
110
|
+
"""Load port assignments from state file."""
|
111
|
+
try:
|
112
|
+
state_path = Path(self.state_file)
|
113
|
+
if state_path.exists():
|
114
|
+
with open(state_path, 'r') as f:
|
115
|
+
self._used_ports = json.load(f)
|
116
|
+
logger.info(f"Loaded port assignments for {len(self._used_ports)} VMs")
|
117
|
+
else:
|
118
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
119
|
+
self._save_state()
|
120
|
+
except Exception as e:
|
121
|
+
logger.error(f"Failed to load port state: {e}")
|
122
|
+
self._used_ports = {}
|
123
|
+
|
124
|
+
def _save_state(self) -> None:
|
125
|
+
"""Save current port assignments to state file."""
|
126
|
+
try:
|
127
|
+
with open(self.state_file, 'w') as f:
|
128
|
+
json.dump(self._used_ports, f)
|
129
|
+
except Exception as e:
|
130
|
+
logger.error(f"Failed to save port state: {e}")
|
131
|
+
|
132
|
+
def _get_used_ports(self) -> Set[int]:
|
133
|
+
"""Get set of currently used ports."""
|
134
|
+
return set(self._used_ports.values())
|
135
|
+
|
136
|
+
def allocate_port(self, vm_id: str) -> Optional[int]:
|
137
|
+
"""Allocate a verified port for a VM.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
vm_id: Unique identifier for the VM
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Allocated port number or None if allocation failed
|
144
|
+
"""
|
145
|
+
with self.lock:
|
146
|
+
# Check if VM already has a port
|
147
|
+
if vm_id in self._used_ports:
|
148
|
+
port = self._used_ports[vm_id]
|
149
|
+
if port in self.verified_ports:
|
150
|
+
return port
|
151
|
+
else:
|
152
|
+
# Previously allocated port is no longer verified
|
153
|
+
self._used_ports.pop(vm_id)
|
154
|
+
|
155
|
+
used_ports = self._get_used_ports()
|
156
|
+
|
157
|
+
# Find first available verified port
|
158
|
+
for port in sorted(self.verified_ports):
|
159
|
+
if port not in used_ports:
|
160
|
+
self._used_ports[vm_id] = port
|
161
|
+
self._save_state()
|
162
|
+
logger.info(f"Allocated verified port {port} for VM {vm_id}")
|
163
|
+
return port
|
164
|
+
|
165
|
+
logger.error("No verified ports available for allocation")
|
166
|
+
return None
|
167
|
+
|
168
|
+
def deallocate_port(self, vm_id: str) -> None:
|
169
|
+
"""Release a port allocation for a VM.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
vm_id: Unique identifier for the VM
|
173
|
+
"""
|
174
|
+
with self.lock:
|
175
|
+
if vm_id in self._used_ports:
|
176
|
+
port = self._used_ports.pop(vm_id)
|
177
|
+
self._save_state()
|
178
|
+
logger.info(f"Deallocated port {port} for VM {vm_id}")
|
179
|
+
|
180
|
+
def get_port(self, vm_id: str) -> Optional[int]:
|
181
|
+
"""Get currently allocated port for a VM.
|
182
|
+
|
183
|
+
Args:
|
184
|
+
vm_id: Unique identifier for the VM
|
185
|
+
|
186
|
+
Returns:
|
187
|
+
Port number or None if VM has no allocation
|
188
|
+
"""
|
189
|
+
return self._used_ports.get(vm_id)
|
190
|
+
|
191
|
+
def cleanup(self) -> None:
|
192
|
+
"""Remove all port allocations."""
|
193
|
+
with self.lock:
|
194
|
+
self._used_ports.clear()
|
195
|
+
self._save_state()
|
196
|
+
logger.info("Cleared all port allocations")
|
@@ -0,0 +1,239 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import asyncio
|
4
|
+
import logging
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Optional, Dict
|
7
|
+
from asyncio import Task, Transport, Protocol
|
8
|
+
|
9
|
+
from .port_manager import PortManager
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
class SSHProxyProtocol(Protocol):
|
14
|
+
"""Protocol for handling SSH proxy connections."""
|
15
|
+
|
16
|
+
def __init__(self, target_host: str, target_port: int):
|
17
|
+
self.target_host = target_host
|
18
|
+
self.target_port = target_port
|
19
|
+
self.transport: Optional[Transport] = None
|
20
|
+
self.target_transport: Optional[Transport] = None
|
21
|
+
self.target_protocol: Optional['SSHTargetProtocol'] = None
|
22
|
+
self.buffer = bytearray()
|
23
|
+
|
24
|
+
def connection_made(self, transport: Transport) -> None:
|
25
|
+
"""Called when connection is established."""
|
26
|
+
self.transport = transport
|
27
|
+
asyncio.create_task(self.connect_to_target())
|
28
|
+
|
29
|
+
async def connect_to_target(self) -> None:
|
30
|
+
"""Establish connection to target."""
|
31
|
+
try:
|
32
|
+
loop = asyncio.get_running_loop()
|
33
|
+
self.target_protocol = SSHTargetProtocol(self)
|
34
|
+
target_transport, _ = await loop.create_connection(
|
35
|
+
lambda: self.target_protocol,
|
36
|
+
self.target_host,
|
37
|
+
self.target_port
|
38
|
+
)
|
39
|
+
self.target_transport = target_transport
|
40
|
+
# If we have buffered data, send it now
|
41
|
+
if self.buffer:
|
42
|
+
self.target_transport.write(self.buffer)
|
43
|
+
self.buffer.clear()
|
44
|
+
except Exception as e:
|
45
|
+
logger.error(f"Failed to connect to target {self.target_host}:{self.target_port}: {e}")
|
46
|
+
if self.transport:
|
47
|
+
self.transport.close()
|
48
|
+
|
49
|
+
def data_received(self, data: bytes) -> None:
|
50
|
+
"""Forward received data to target."""
|
51
|
+
if self.target_transport and not self.target_transport.is_closing():
|
52
|
+
self.target_transport.write(data)
|
53
|
+
else:
|
54
|
+
# Buffer data until target connection is established
|
55
|
+
self.buffer.extend(data)
|
56
|
+
|
57
|
+
def connection_lost(self, exc: Optional[Exception]) -> None:
|
58
|
+
"""Handle connection loss."""
|
59
|
+
if exc:
|
60
|
+
logger.error(f"Client connection lost with error: {exc}")
|
61
|
+
if self.target_transport and not self.target_transport.is_closing():
|
62
|
+
self.target_transport.close()
|
63
|
+
|
64
|
+
class SSHTargetProtocol(Protocol):
|
65
|
+
"""Protocol for handling target SSH connections."""
|
66
|
+
|
67
|
+
def __init__(self, client_protocol: SSHProxyProtocol):
|
68
|
+
self.client_protocol = client_protocol
|
69
|
+
self.transport: Optional[Transport] = None
|
70
|
+
|
71
|
+
def connection_made(self, transport: Transport) -> None:
|
72
|
+
"""Called when connection is established."""
|
73
|
+
self.transport = transport
|
74
|
+
|
75
|
+
def data_received(self, data: bytes) -> None:
|
76
|
+
"""Forward received data to client."""
|
77
|
+
if (self.client_protocol.transport and
|
78
|
+
not self.client_protocol.transport.is_closing()):
|
79
|
+
self.client_protocol.transport.write(data)
|
80
|
+
|
81
|
+
def connection_lost(self, exc: Optional[Exception]) -> None:
|
82
|
+
"""Handle connection loss."""
|
83
|
+
if exc:
|
84
|
+
logger.error(f"Target connection lost with error: {exc}")
|
85
|
+
if (self.client_protocol.transport and
|
86
|
+
not self.client_protocol.transport.is_closing()):
|
87
|
+
self.client_protocol.transport.close()
|
88
|
+
|
89
|
+
class ProxyServer:
|
90
|
+
"""Manages a single proxy server instance."""
|
91
|
+
|
92
|
+
def __init__(self, listen_port: int, target_host: str, target_port: int = 22):
|
93
|
+
"""Initialize proxy server.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
listen_port: Port to listen on
|
97
|
+
target_host: Target host to forward to
|
98
|
+
target_port: Target port (default: 22 for SSH)
|
99
|
+
"""
|
100
|
+
self.listen_port = listen_port
|
101
|
+
self.target_host = target_host
|
102
|
+
self.target_port = target_port
|
103
|
+
self.server: Optional[asyncio.AbstractServer] = None
|
104
|
+
|
105
|
+
async def start(self) -> None:
|
106
|
+
"""Start the proxy server."""
|
107
|
+
loop = asyncio.get_running_loop()
|
108
|
+
|
109
|
+
try:
|
110
|
+
self.server = await loop.create_server(
|
111
|
+
lambda: SSHProxyProtocol(self.target_host, self.target_port),
|
112
|
+
'0.0.0.0', # Listen on all interfaces
|
113
|
+
self.listen_port
|
114
|
+
)
|
115
|
+
logger.info(f"Proxy server listening on port {self.listen_port}")
|
116
|
+
except Exception as e:
|
117
|
+
logger.error(f"Failed to start proxy server on port {self.listen_port}: {e}")
|
118
|
+
raise
|
119
|
+
|
120
|
+
async def stop(self) -> None:
|
121
|
+
"""Stop the proxy server."""
|
122
|
+
if self.server:
|
123
|
+
self.server.close()
|
124
|
+
await self.server.wait_closed()
|
125
|
+
logger.info(f"Proxy server on port {self.listen_port} stopped")
|
126
|
+
|
127
|
+
class PythonProxyManager:
|
128
|
+
"""Manages proxy servers for VM SSH access."""
|
129
|
+
|
130
|
+
def __init__(
|
131
|
+
self,
|
132
|
+
port_manager: Optional[PortManager] = None,
|
133
|
+
state_file: Optional[str] = None
|
134
|
+
):
|
135
|
+
"""Initialize the proxy manager.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
port_manager: Port allocation manager
|
139
|
+
state_file: Path to persist proxy state
|
140
|
+
"""
|
141
|
+
self.port_manager = port_manager or PortManager()
|
142
|
+
self.state_file = state_file or os.path.expanduser("~/.golem/provider/proxy_state.json")
|
143
|
+
self._proxies: Dict[str, ProxyServer] = {} # vm_id -> ProxyServer
|
144
|
+
self._load_state()
|
145
|
+
|
146
|
+
def _load_state(self) -> None:
|
147
|
+
"""Load proxy state from file."""
|
148
|
+
try:
|
149
|
+
state_path = Path(self.state_file)
|
150
|
+
if state_path.exists():
|
151
|
+
with open(state_path, 'r') as f:
|
152
|
+
state = json.load(f)
|
153
|
+
# We only need to restore port allocations
|
154
|
+
# Actual proxy servers will be recreated as needed
|
155
|
+
logger.info(f"Loaded proxy state for {len(state)} VMs")
|
156
|
+
except Exception as e:
|
157
|
+
logger.error(f"Failed to load proxy state: {e}")
|
158
|
+
|
159
|
+
def _save_state(self) -> None:
|
160
|
+
"""Save current proxy state to file."""
|
161
|
+
try:
|
162
|
+
state = {
|
163
|
+
vm_id: {
|
164
|
+
'port': proxy.listen_port,
|
165
|
+
'target': proxy.target_host
|
166
|
+
}
|
167
|
+
for vm_id, proxy in self._proxies.items()
|
168
|
+
}
|
169
|
+
os.makedirs(os.path.dirname(self.state_file), exist_ok=True)
|
170
|
+
with open(self.state_file, 'w') as f:
|
171
|
+
json.dump(state, f)
|
172
|
+
except Exception as e:
|
173
|
+
logger.error(f"Failed to save proxy state: {e}")
|
174
|
+
|
175
|
+
async def add_vm(self, vm_id: str, vm_ip: str, port: Optional[int] = None) -> bool:
|
176
|
+
"""Add proxy configuration for a new VM.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
vm_id: Unique identifier for the VM
|
180
|
+
vm_ip: IP address of the VM
|
181
|
+
port: Optional specific port to use, if not provided one will be allocated
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
True if proxy configuration was successful, False otherwise
|
185
|
+
"""
|
186
|
+
try:
|
187
|
+
# Use provided port or allocate one
|
188
|
+
if port is None:
|
189
|
+
port = self.port_manager.allocate_port(vm_id)
|
190
|
+
if port is None:
|
191
|
+
logger.error(f"Failed to allocate port for VM {vm_id}")
|
192
|
+
return False
|
193
|
+
|
194
|
+
# Create and start proxy server
|
195
|
+
proxy = ProxyServer(port, vm_ip)
|
196
|
+
await proxy.start()
|
197
|
+
|
198
|
+
self._proxies[vm_id] = proxy
|
199
|
+
self._save_state()
|
200
|
+
|
201
|
+
logger.info(f"Started proxy for VM {vm_id} on port {port}")
|
202
|
+
return True
|
203
|
+
|
204
|
+
except Exception as e:
|
205
|
+
logger.error(f"Failed to configure proxy for VM {vm_id}: {e}")
|
206
|
+
# Only deallocate if we allocated the port ourselves
|
207
|
+
if port is None and 'port' in locals():
|
208
|
+
self.port_manager.deallocate_port(vm_id)
|
209
|
+
return False
|
210
|
+
|
211
|
+
async def remove_vm(self, vm_id: str) -> None:
|
212
|
+
"""Remove proxy configuration for a VM.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
vm_id: Unique identifier for the VM
|
216
|
+
"""
|
217
|
+
try:
|
218
|
+
if vm_id in self._proxies:
|
219
|
+
proxy = self._proxies.pop(vm_id)
|
220
|
+
await proxy.stop()
|
221
|
+
self.port_manager.deallocate_port(vm_id)
|
222
|
+
self._save_state()
|
223
|
+
logger.info(f"Removed proxy for VM {vm_id}")
|
224
|
+
except Exception as e:
|
225
|
+
logger.error(f"Failed to remove proxy for VM {vm_id}: {e}")
|
226
|
+
|
227
|
+
def get_port(self, vm_id: str) -> Optional[int]:
|
228
|
+
"""Get allocated port for a VM."""
|
229
|
+
return self.port_manager.get_port(vm_id)
|
230
|
+
|
231
|
+
async def cleanup(self) -> None:
|
232
|
+
"""Remove all proxy configurations."""
|
233
|
+
try:
|
234
|
+
for vm_id in list(self._proxies.keys()):
|
235
|
+
await self.remove_vm(vm_id)
|
236
|
+
self._save_state()
|
237
|
+
logger.info("Cleaned up all proxy configurations")
|
238
|
+
except Exception as e:
|
239
|
+
logger.error(f"Failed to cleanup proxy configurations: {e}")
|