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.
@@ -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}")