request-vm-on-golem 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.
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.27.dist-info}/METADATA +11 -9
- request_vm_on_golem-0.1.27.dist-info/RECORD +24 -0
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.27.dist-info}/WHEEL +1 -1
- requestor/api/main.py +59 -0
- requestor/cli/commands.py +220 -273
- requestor/config.py +36 -2
- requestor/db/sqlite.py +33 -41
- requestor/run.py +4 -2
- requestor/services/__init__.py +6 -0
- requestor/services/database_service.py +91 -0
- requestor/services/provider_service.py +265 -0
- requestor/services/ssh_service.py +128 -0
- requestor/services/vm_service.py +218 -0
- requestor/ssh/manager.py +62 -0
- request_vm_on_golem-0.1.24.dist-info/RECORD +0 -18
- {request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.27.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,218 @@
|
|
1
|
+
"""VM management service."""
|
2
|
+
from typing import Dict, List, Optional
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
from ..provider.client import ProviderClient
|
6
|
+
from ..errors import RequestorError, VMError
|
7
|
+
from .database_service import DatabaseService
|
8
|
+
from .ssh_service import SSHService
|
9
|
+
|
10
|
+
class VMService:
|
11
|
+
"""Service for VM operations."""
|
12
|
+
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
db_service: DatabaseService,
|
16
|
+
ssh_service: SSHService,
|
17
|
+
provider_client: Optional[ProviderClient] = None
|
18
|
+
):
|
19
|
+
self.db = db_service
|
20
|
+
self.ssh_service = ssh_service
|
21
|
+
self.provider_client = provider_client
|
22
|
+
|
23
|
+
async def create_vm(
|
24
|
+
self,
|
25
|
+
name: str,
|
26
|
+
cpu: int,
|
27
|
+
memory: int,
|
28
|
+
storage: int,
|
29
|
+
provider_ip: str,
|
30
|
+
ssh_key: str
|
31
|
+
) -> Dict:
|
32
|
+
"""Create a new VM with validation and error handling."""
|
33
|
+
try:
|
34
|
+
# Check if VM name already exists
|
35
|
+
existing_vm = await self.db.get_vm(name)
|
36
|
+
if existing_vm:
|
37
|
+
raise VMError(f"VM with name '{name}' already exists")
|
38
|
+
|
39
|
+
# Create VM on provider
|
40
|
+
vm = await self.provider_client.create_vm(
|
41
|
+
name=name,
|
42
|
+
cpu=cpu,
|
43
|
+
memory=memory,
|
44
|
+
storage=storage,
|
45
|
+
ssh_key=ssh_key
|
46
|
+
)
|
47
|
+
|
48
|
+
# Get VM access info
|
49
|
+
access_info = await self.provider_client.get_vm_access(vm['id'])
|
50
|
+
|
51
|
+
# Save VM details to database
|
52
|
+
config = {
|
53
|
+
'cpu': cpu,
|
54
|
+
'memory': memory,
|
55
|
+
'storage': storage,
|
56
|
+
'ssh_port': access_info['ssh_port']
|
57
|
+
}
|
58
|
+
await self.db.save_vm(
|
59
|
+
name=name,
|
60
|
+
provider_ip=provider_ip,
|
61
|
+
vm_id=access_info['vm_id'],
|
62
|
+
config=config
|
63
|
+
)
|
64
|
+
|
65
|
+
return {
|
66
|
+
'name': name,
|
67
|
+
'provider_ip': provider_ip,
|
68
|
+
'vm_id': access_info['vm_id'],
|
69
|
+
'config': config,
|
70
|
+
'status': 'running'
|
71
|
+
}
|
72
|
+
|
73
|
+
except Exception as e:
|
74
|
+
raise VMError(f"Failed to create VM: {str(e)}")
|
75
|
+
|
76
|
+
async def destroy_vm(self, name: str) -> None:
|
77
|
+
"""Destroy a VM and clean up resources."""
|
78
|
+
try:
|
79
|
+
# Get VM details
|
80
|
+
vm = await self.db.get_vm(name)
|
81
|
+
if not vm:
|
82
|
+
raise VMError(f"VM '{name}' not found")
|
83
|
+
|
84
|
+
try:
|
85
|
+
# Destroy VM on provider
|
86
|
+
await self.provider_client.destroy_vm(vm['vm_id'])
|
87
|
+
except Exception as e:
|
88
|
+
if "Not Found" not in str(e):
|
89
|
+
raise
|
90
|
+
|
91
|
+
# Remove from database
|
92
|
+
await self.db.delete_vm(name)
|
93
|
+
|
94
|
+
except Exception as e:
|
95
|
+
raise VMError(f"Failed to destroy VM: {str(e)}")
|
96
|
+
|
97
|
+
async def start_vm(self, name: str) -> None:
|
98
|
+
"""Start a stopped VM."""
|
99
|
+
try:
|
100
|
+
# Get VM details
|
101
|
+
vm = await self.db.get_vm(name)
|
102
|
+
if not vm:
|
103
|
+
raise VMError(f"VM '{name}' not found")
|
104
|
+
|
105
|
+
# Start VM on provider
|
106
|
+
await self.provider_client.start_vm(vm['vm_id'])
|
107
|
+
|
108
|
+
# Update status in database
|
109
|
+
await self.db.update_vm_status(name, "running")
|
110
|
+
|
111
|
+
except Exception as e:
|
112
|
+
raise VMError(f"Failed to start VM: {str(e)}")
|
113
|
+
|
114
|
+
async def stop_vm(self, name: str) -> None:
|
115
|
+
"""Stop a running VM."""
|
116
|
+
try:
|
117
|
+
# Get VM details
|
118
|
+
vm = await self.db.get_vm(name)
|
119
|
+
if not vm:
|
120
|
+
raise VMError(f"VM '{name}' not found")
|
121
|
+
|
122
|
+
# Stop VM on provider
|
123
|
+
await self.provider_client.stop_vm(vm['vm_id'])
|
124
|
+
|
125
|
+
# Update status in database
|
126
|
+
await self.db.update_vm_status(name, "stopped")
|
127
|
+
|
128
|
+
except Exception as e:
|
129
|
+
raise VMError(f"Failed to stop VM: {str(e)}")
|
130
|
+
|
131
|
+
async def list_vms(self) -> List[Dict]:
|
132
|
+
"""List all VMs with their current status."""
|
133
|
+
try:
|
134
|
+
return await self.db.list_vms()
|
135
|
+
except Exception as e:
|
136
|
+
raise VMError(f"Failed to list VMs: {str(e)}")
|
137
|
+
|
138
|
+
async def get_vm(self, name: str) -> Optional[Dict]:
|
139
|
+
"""Get VM details by name."""
|
140
|
+
try:
|
141
|
+
vm = await self.db.get_vm(name)
|
142
|
+
if not vm:
|
143
|
+
return None
|
144
|
+
return vm
|
145
|
+
except Exception as e:
|
146
|
+
raise VMError(f"Failed to get VM details: {str(e)}")
|
147
|
+
|
148
|
+
def format_vm_row(self, vm: Dict, colorize: bool = False) -> List:
|
149
|
+
"""Format VM information for display."""
|
150
|
+
from click import style
|
151
|
+
|
152
|
+
key_pair = self.ssh_service.get_key_pair_sync()
|
153
|
+
connect_command = self.ssh_service.format_ssh_command(
|
154
|
+
host=vm['provider_ip'],
|
155
|
+
port=vm['config'].get('ssh_port', 'N/A'),
|
156
|
+
private_key_path=key_pair.private_key.absolute()
|
157
|
+
)
|
158
|
+
|
159
|
+
row = [
|
160
|
+
vm['name'],
|
161
|
+
vm['status'],
|
162
|
+
vm['provider_ip'],
|
163
|
+
vm['config'].get('ssh_port', 'N/A'),
|
164
|
+
vm['config']['cpu'],
|
165
|
+
vm['config']['memory'],
|
166
|
+
vm['config']['storage'],
|
167
|
+
connect_command,
|
168
|
+
vm['created_at']
|
169
|
+
]
|
170
|
+
|
171
|
+
if colorize:
|
172
|
+
# Format status with color and icon
|
173
|
+
status = row[1]
|
174
|
+
if status == "running":
|
175
|
+
row[1] = style("● " + status, fg="green", bold=True)
|
176
|
+
elif status == "stopped":
|
177
|
+
row[1] = style("● " + status, fg="yellow", bold=True)
|
178
|
+
else:
|
179
|
+
row[1] = style("● " + status, fg="red", bold=True)
|
180
|
+
|
181
|
+
# Format other columns
|
182
|
+
row[0] = style(row[0], fg="cyan") # Name
|
183
|
+
row[2] = style(row[2], fg="cyan") # IP
|
184
|
+
row[3] = style(str(row[3]), fg="cyan") # Port
|
185
|
+
|
186
|
+
return row
|
187
|
+
|
188
|
+
@property
|
189
|
+
def vm_headers(self) -> List[str]:
|
190
|
+
"""Get headers for VM display."""
|
191
|
+
return [
|
192
|
+
"Name",
|
193
|
+
"Status",
|
194
|
+
"IP Address",
|
195
|
+
"SSH Port",
|
196
|
+
"CPU",
|
197
|
+
"Memory (GB)",
|
198
|
+
"Storage (GB)",
|
199
|
+
"Connect Command",
|
200
|
+
"Created"
|
201
|
+
]
|
202
|
+
|
203
|
+
async def get_vm_stats(self, name: str) -> Dict:
|
204
|
+
"""Get VM stats by name."""
|
205
|
+
try:
|
206
|
+
vm = await self.db.get_vm(name)
|
207
|
+
if not vm:
|
208
|
+
raise VMError(f"VM '{name}' not found")
|
209
|
+
|
210
|
+
key_pair = await self.ssh_service.get_key_pair()
|
211
|
+
|
212
|
+
return self.ssh_service.get_vm_stats(
|
213
|
+
host=vm['provider_ip'],
|
214
|
+
port=vm['config']['ssh_port'],
|
215
|
+
private_key_path=key_pair.private_key
|
216
|
+
)
|
217
|
+
except Exception as e:
|
218
|
+
raise VMError(f"Failed to get VM stats: {str(e)}")
|
requestor/ssh/manager.py
CHANGED
@@ -93,6 +93,36 @@ class SSHKeyManager:
|
|
93
93
|
public_key_content=golem_pub_key.read_text().strip()
|
94
94
|
)
|
95
95
|
|
96
|
+
def get_key_pair_sync(self) -> KeyPair:
|
97
|
+
"""Get the SSH key pair to use (synchronous version)."""
|
98
|
+
logger.debug("Checking for system SSH key at %s", self.system_key_path)
|
99
|
+
system_pub_key = self.system_key_path.parent / 'id_rsa.pub'
|
100
|
+
|
101
|
+
if self.system_key_path.exists() and system_pub_key.exists():
|
102
|
+
logger.info("Using existing system SSH key")
|
103
|
+
try:
|
104
|
+
return KeyPair(
|
105
|
+
private_key=self.system_key_path,
|
106
|
+
public_key=system_pub_key,
|
107
|
+
private_key_content=self.system_key_path.read_text().strip(),
|
108
|
+
public_key_content=system_pub_key.read_text().strip()
|
109
|
+
)
|
110
|
+
except (PermissionError, OSError) as e:
|
111
|
+
logger.warning("Could not read system SSH key: %s", e)
|
112
|
+
|
113
|
+
logger.debug("Using Golem SSH key at %s", self.golem_key_path)
|
114
|
+
if not self.golem_key_path.exists():
|
115
|
+
logger.info("No existing Golem SSH key found, generating new key pair")
|
116
|
+
self._generate_key_pair_sync()
|
117
|
+
|
118
|
+
golem_pub_key = Path(str(self.golem_key_path) + '.pub')
|
119
|
+
return KeyPair(
|
120
|
+
private_key=self.golem_key_path,
|
121
|
+
public_key=golem_pub_key,
|
122
|
+
private_key_content=self.golem_key_path.read_text().strip(),
|
123
|
+
public_key_content=golem_pub_key.read_text().strip()
|
124
|
+
)
|
125
|
+
|
96
126
|
async def get_public_key_content(self) -> str:
|
97
127
|
"""Get the content of the public key file."""
|
98
128
|
key_pair = await self.get_key_pair()
|
@@ -155,6 +185,38 @@ class SSHKeyManager:
|
|
155
185
|
logger.error("Failed to generate key pair: %s", str(e))
|
156
186
|
raise
|
157
187
|
|
188
|
+
def _generate_key_pair_sync(self):
|
189
|
+
"""Generate a new RSA key pair for Golem VMs (synchronous version)."""
|
190
|
+
logger.debug("Generating new RSA key pair")
|
191
|
+
try:
|
192
|
+
private_key = rsa.generate_private_key(
|
193
|
+
public_exponent=65537,
|
194
|
+
key_size=2048,
|
195
|
+
backend=default_backend()
|
196
|
+
)
|
197
|
+
private_pem = private_key.private_bytes(
|
198
|
+
encoding=serialization.Encoding.PEM,
|
199
|
+
format=serialization.PrivateFormat.PKCS8,
|
200
|
+
encryption_algorithm=serialization.NoEncryption()
|
201
|
+
)
|
202
|
+
self.golem_key_path.write_bytes(private_pem)
|
203
|
+
if os.name == 'posix':
|
204
|
+
os.chmod(self.golem_key_path, 0o600)
|
205
|
+
|
206
|
+
public_key = private_key.public_key()
|
207
|
+
public_pem = public_key.public_bytes(
|
208
|
+
encoding=serialization.Encoding.OpenSSH,
|
209
|
+
format=serialization.PublicFormat.OpenSSH
|
210
|
+
)
|
211
|
+
pub_key_path = Path(str(self.golem_key_path) + '.pub')
|
212
|
+
pub_key_path.write_bytes(public_pem)
|
213
|
+
if os.name == 'posix':
|
214
|
+
os.chmod(pub_key_path, 0o644)
|
215
|
+
logger.info("Successfully generated and saved SSH key pair")
|
216
|
+
except Exception as e:
|
217
|
+
logger.error("Failed to generate key pair: %s", str(e))
|
218
|
+
raise
|
219
|
+
|
158
220
|
async def get_private_key_content(self, force_golem_key: bool = False) -> Optional[str]:
|
159
221
|
"""Get the content of the private key file."""
|
160
222
|
key_pair = await self.get_key_pair(force_golem_key)
|
@@ -1,18 +0,0 @@
|
|
1
|
-
requestor/__init__.py,sha256=OqSUAh1uZBMx7GW0MoSMg967PVdmT8XdPJx3QYjwkak,116
|
2
|
-
requestor/cli/__init__.py,sha256=e3E4oEGxmGj-STPtFkQwg_qIWhR0JAiAQdw3G1hXciU,37
|
3
|
-
requestor/cli/commands.py,sha256=deW4HxJGJvWQCOEvkuPEzkIfSDOSYa2npO4i_4sesCo,24283
|
4
|
-
requestor/config.py,sha256=3e0xAuteZ3JiE6uWHipQLQQklGN59hiqdqZUT2mUutM,2146
|
5
|
-
requestor/db/__init__.py,sha256=Gm5DfWls6uvCZZ3HGGnyRHswbUQdeA5OGN8yPwH0hc8,88
|
6
|
-
requestor/db/sqlite.py,sha256=kqutQMs3srcZHa5kI1cbHvOYr66cCfQCb2xuioNAbLc,4638
|
7
|
-
requestor/errors.py,sha256=wVpHBuYgQx5pTe_SamugfK-k768noikY1RxvPOjQGko,665
|
8
|
-
requestor/provider/__init__.py,sha256=fmW23aYUVciF8-gmBZkG-PLhn22upmcDzdPfAOLHG6g,103
|
9
|
-
requestor/provider/client.py,sha256=OUP7CoOCCtKD6DB9eqFkOXK6A2BLFdM4DWSkoulJQxg,3213
|
10
|
-
requestor/run.py,sha256=mFqoP93KuRpQZ8pmzP5pHvByaDjHJ9xRYRjYC77s6bg,1386
|
11
|
-
requestor/ssh/__init__.py,sha256=hNgSqJ5s1_AwwxVRyFjUqh_LTBpI4Hmzq0F-f_wXN9g,119
|
12
|
-
requestor/ssh/manager.py,sha256=h-93AXFJqzGo2lNTG2u-q4ivU9cCFeNDhYN55skPLBo,6566
|
13
|
-
requestor/utils/logging.py,sha256=oFNpO8pJboYM8Wp7g3HOU4HFyBTKypVdY15lUiz1a4I,3721
|
14
|
-
requestor/utils/spinner.py,sha256=PUHJdTD9jpUHur__01_qxXy87WFfNmjQbD_sLG-KlGo,2459
|
15
|
-
request_vm_on_golem-0.1.24.dist-info/METADATA,sha256=969OAj0KxwHXeI6BWZkfiLbjyO-sYdBPxlDyEP9QyTE,9060
|
16
|
-
request_vm_on_golem-0.1.24.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
17
|
-
request_vm_on_golem-0.1.24.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
|
18
|
-
request_vm_on_golem-0.1.24.dist-info/RECORD,,
|
{request_vm_on_golem-0.1.24.dist-info → request_vm_on_golem-0.1.27.dist-info}/entry_points.txt
RENAMED
File without changes
|