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,204 @@
|
|
1
|
+
"""Port verification display utilities."""
|
2
|
+
import time
|
3
|
+
import sys
|
4
|
+
import asyncio
|
5
|
+
from typing import Dict, List, Optional
|
6
|
+
from ..network.port_verifier import PortVerificationResult
|
7
|
+
|
8
|
+
class PortVerificationDisplay:
|
9
|
+
"""Display utilities for port verification status."""
|
10
|
+
|
11
|
+
SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
12
|
+
|
13
|
+
def __init__(self, provider_port: int, port_range_start: int, port_range_end: int):
|
14
|
+
"""Initialize the display.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
provider_port: Port used for provider access
|
18
|
+
port_range_start: Start of VM access port range
|
19
|
+
port_range_end: End of VM access port range
|
20
|
+
"""
|
21
|
+
self.provider_port = provider_port
|
22
|
+
self.port_range_start = port_range_start
|
23
|
+
self.port_range_end = port_range_end
|
24
|
+
self.spinner_idx = 0
|
25
|
+
|
26
|
+
def _update_spinner(self):
|
27
|
+
"""Update and return the next spinner frame."""
|
28
|
+
frame = self.SPINNER_FRAMES[self.spinner_idx]
|
29
|
+
self.spinner_idx = (self.spinner_idx + 1) % len(self.SPINNER_FRAMES)
|
30
|
+
return frame
|
31
|
+
|
32
|
+
async def animate_verification(self, text: str, duration: float = 1.0):
|
33
|
+
"""Show an animated spinner while verifying.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
text: Text to show with spinner
|
37
|
+
duration: How long to show the animation
|
38
|
+
"""
|
39
|
+
start_time = time.time()
|
40
|
+
while time.time() - start_time < duration:
|
41
|
+
sys.stdout.write(f"\r{self._update_spinner()} {text}")
|
42
|
+
sys.stdout.flush()
|
43
|
+
await asyncio.sleep(0.1)
|
44
|
+
sys.stdout.write("\r" + " " * (len(text) + 2) + "\r")
|
45
|
+
sys.stdout.flush()
|
46
|
+
|
47
|
+
def print_header(self):
|
48
|
+
"""Print the verification status header."""
|
49
|
+
print("\n🌟 Port Verification Status")
|
50
|
+
print("==========================")
|
51
|
+
|
52
|
+
async def print_discovery_status(self, result: PortVerificationResult):
|
53
|
+
"""Print discovery service status with tree structure.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
result: Verification result for discovery port
|
57
|
+
"""
|
58
|
+
print("\n📡 Provider Accessibility (Required)")
|
59
|
+
print("--------------------------------")
|
60
|
+
|
61
|
+
await self.animate_verification("Checking provider accessibility...")
|
62
|
+
|
63
|
+
status_badge = "✅ Accessible" if result.accessible else "❌ Not Accessible"
|
64
|
+
print(f"[{status_badge}] Port {self.provider_port}")
|
65
|
+
print(f"└─ Status: {'Accessible' if result.accessible else 'Not Accessible'}")
|
66
|
+
|
67
|
+
# Show external/internal access
|
68
|
+
if result.accessible:
|
69
|
+
print("└─ Access: External ✓ | Internal ✓")
|
70
|
+
print("└─ Requestors can discover and connect to your provider")
|
71
|
+
else:
|
72
|
+
print("└─ Access: External ✗ | Internal ✗")
|
73
|
+
print("└─ Requestors cannot discover or connect to your provider")
|
74
|
+
|
75
|
+
# Show verification server if successful
|
76
|
+
if result.verified_by:
|
77
|
+
print(f"└─ Verified By: {result.verified_by}")
|
78
|
+
|
79
|
+
async def print_ssh_status(self, results: Dict[int, PortVerificationResult]):
|
80
|
+
"""Print SSH ports status with progress bar.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
results: Dictionary mapping ports to their verification results
|
84
|
+
"""
|
85
|
+
print("\n🔒 VM Access Ports (Required)")
|
86
|
+
print("-------------------------")
|
87
|
+
|
88
|
+
await self.animate_verification("Scanning VM access ports...")
|
89
|
+
|
90
|
+
# Calculate progress
|
91
|
+
total_ports = len(results)
|
92
|
+
accessible_ports = sum(1 for r in results.values() if r.accessible)
|
93
|
+
percentage = (accessible_ports / total_ports) * 100 if total_ports > 0 else 0
|
94
|
+
|
95
|
+
# Create animated progress bar
|
96
|
+
bar_width = 30
|
97
|
+
for i in range(bar_width + 1):
|
98
|
+
filled = int(i * percentage / 100)
|
99
|
+
bar = "█" * filled + "░" * (bar_width - filled)
|
100
|
+
sys.stdout.write(f"\r[{bar}] {percentage:.1f}%")
|
101
|
+
sys.stdout.flush()
|
102
|
+
time.sleep(0.02)
|
103
|
+
print()
|
104
|
+
|
105
|
+
# List available ports
|
106
|
+
available = [port for port, result in results.items() if result.accessible]
|
107
|
+
if available:
|
108
|
+
print(f"\nAvailable Ports: {', '.join(map(str, sorted(available)))}")
|
109
|
+
else:
|
110
|
+
print("\nAvailable Ports: None")
|
111
|
+
|
112
|
+
print(f"Required: At least 1 port in range {self.port_range_start}-{self.port_range_end}")
|
113
|
+
|
114
|
+
def print_critical_issues(self, discovery_result: PortVerificationResult,
|
115
|
+
ssh_results: Dict[int, PortVerificationResult]):
|
116
|
+
"""Print critical issues with actionable items.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
discovery_result: Verification result for discovery port
|
120
|
+
ssh_results: Dictionary mapping SSH ports to their verification results
|
121
|
+
"""
|
122
|
+
issues = []
|
123
|
+
|
124
|
+
# Check discovery port
|
125
|
+
if not discovery_result.accessible:
|
126
|
+
issues.append((f"Port {self.provider_port} is not accessible",
|
127
|
+
"Requestors cannot discover or connect to your provider",
|
128
|
+
f"Configure port forwarding for port {self.provider_port}"))
|
129
|
+
|
130
|
+
# Check SSH ports
|
131
|
+
if not any(r.accessible for r in ssh_results.values()):
|
132
|
+
issues.append(("No VM access ports are accessible",
|
133
|
+
"Requestors will not be able to access their rented VMs",
|
134
|
+
f"Configure port forwarding for range {self.port_range_start}-{self.port_range_end}"))
|
135
|
+
|
136
|
+
if issues:
|
137
|
+
print("\n🚨 Critical Issues")
|
138
|
+
print("---------------")
|
139
|
+
for i, (issue, impact, action) in enumerate(issues, 1):
|
140
|
+
print(f"{i}. {issue}")
|
141
|
+
print(f" ↳ {impact}")
|
142
|
+
print(f" ↳ Action: {action}")
|
143
|
+
|
144
|
+
def print_quick_fix(self, discovery_result: PortVerificationResult,
|
145
|
+
ssh_results: Dict[int, PortVerificationResult]):
|
146
|
+
"""Print quick fix guide only if there are issues.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
discovery_result: Verification result for discovery port
|
150
|
+
ssh_results: Dictionary mapping SSH ports to their verification results
|
151
|
+
"""
|
152
|
+
# Check if we have any issues
|
153
|
+
has_issues = (
|
154
|
+
not discovery_result.accessible or
|
155
|
+
not any(r.accessible for r in ssh_results.values())
|
156
|
+
)
|
157
|
+
|
158
|
+
if has_issues:
|
159
|
+
print("\n💡 Quick Fix Guide")
|
160
|
+
print("---------------")
|
161
|
+
|
162
|
+
print("1. Check your router's port forwarding settings")
|
163
|
+
print(f" ↳ Forward ports {self.port_range_start}-{self.port_range_end} to this machine")
|
164
|
+
print(" ↳ Tutorial: docs.golem.network/port-forwarding")
|
165
|
+
|
166
|
+
print("\n2. Verify firewall rules")
|
167
|
+
print(" ↳ Allow incoming TCP connections on ports:")
|
168
|
+
print(f" • {self.provider_port} (Provider Access)")
|
169
|
+
print(f" • {self.port_range_start}-{self.port_range_end} (VM Access)")
|
170
|
+
print(" ↳ Tutorial: docs.golem.network/firewall-setup")
|
171
|
+
|
172
|
+
print("\nNeed help? Visit our troubleshooting guide: docs.golem.network/ports")
|
173
|
+
|
174
|
+
def print_summary(self, discovery_result: PortVerificationResult,
|
175
|
+
ssh_results: Dict[int, PortVerificationResult]):
|
176
|
+
"""Print a precise, actionable summary of the verification status.
|
177
|
+
|
178
|
+
Args:
|
179
|
+
discovery_result: Verification result for discovery port
|
180
|
+
ssh_results: Dictionary mapping SSH ports to their verification results
|
181
|
+
"""
|
182
|
+
print("\n🎯 Current Status:", end=" ")
|
183
|
+
|
184
|
+
accessible_ssh_ports = [port for port, result in ssh_results.items() if result.accessible]
|
185
|
+
|
186
|
+
if not discovery_result.accessible:
|
187
|
+
print("Provider Not Discoverable")
|
188
|
+
print(f"└─ Reason: Port {self.provider_port} is not accessible")
|
189
|
+
print("└─ Impact: Requestors cannot find or connect to your provider")
|
190
|
+
print(f"└─ Fix: Configure port forwarding for port {self.provider_port}")
|
191
|
+
|
192
|
+
elif not accessible_ssh_ports:
|
193
|
+
print("VMs Not Accessible")
|
194
|
+
print("└─ Reason: No VM access ports are available")
|
195
|
+
print("└─ Impact: Requestors will not be able to access their rented VMs")
|
196
|
+
print(f"└─ Fix: Configure port forwarding for range {self.port_range_start}-{self.port_range_end}")
|
197
|
+
|
198
|
+
else:
|
199
|
+
status = "Provider Ready" if len(accessible_ssh_ports) > 5 else "Provider Ready with Limited Capacity"
|
200
|
+
print(status)
|
201
|
+
print(f"└─ Available: {len(accessible_ssh_ports)} SSH ports ({', '.join(map(str, sorted(accessible_ssh_ports)[:3]))}{'...' if len(accessible_ssh_ports) > 3 else ''})")
|
202
|
+
print(f"└─ Capacity: Can handle up to {len(accessible_ssh_ports)} concurrent VMs")
|
203
|
+
if len(accessible_ssh_ports) <= 5:
|
204
|
+
print("└─ Recommendation: Open more ports for higher capacity")
|
provider/utils/retry.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from functools import wraps
|
4
|
+
from typing import TypeVar, Callable, Any
|
5
|
+
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
T = TypeVar('T')
|
9
|
+
|
10
|
+
def async_retry(
|
11
|
+
retries: int = 3,
|
12
|
+
delay: float = 1.0,
|
13
|
+
backoff: float = 2.0,
|
14
|
+
exceptions: tuple = (Exception,)
|
15
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
16
|
+
"""
|
17
|
+
Retry decorator for async functions with exponential backoff.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
retries: Maximum number of retries
|
21
|
+
delay: Initial delay between retries in seconds
|
22
|
+
backoff: Multiplier for delay after each retry
|
23
|
+
exceptions: Tuple of exceptions to catch
|
24
|
+
"""
|
25
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
26
|
+
@wraps(func)
|
27
|
+
async def wrapper(*args: Any, **kwargs: Any) -> T:
|
28
|
+
current_delay = delay
|
29
|
+
last_exception = None
|
30
|
+
|
31
|
+
for attempt in range(retries + 1):
|
32
|
+
try:
|
33
|
+
return await func(*args, **kwargs)
|
34
|
+
except exceptions as e:
|
35
|
+
last_exception = e
|
36
|
+
if attempt == retries:
|
37
|
+
break
|
38
|
+
|
39
|
+
logger.warning(
|
40
|
+
f"Attempt {attempt + 1}/{retries} failed for {func.__name__}: {e}. "
|
41
|
+
f"Retrying in {current_delay:.1f}s..."
|
42
|
+
)
|
43
|
+
await asyncio.sleep(current_delay)
|
44
|
+
current_delay *= backoff
|
45
|
+
|
46
|
+
raise last_exception
|
47
|
+
return wrapper
|
48
|
+
return decorator
|
provider/vm/__init__.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
from .models import (
|
2
|
+
VMConfig,
|
3
|
+
VMInfo,
|
4
|
+
VMStatus,
|
5
|
+
VMSize,
|
6
|
+
VMResources,
|
7
|
+
SSHKey,
|
8
|
+
VMProvider,
|
9
|
+
VMError,
|
10
|
+
VMCreateError,
|
11
|
+
VMNotFoundError,
|
12
|
+
VMStateError,
|
13
|
+
ResourceError
|
14
|
+
)
|
15
|
+
from .multipass import MultipassProvider
|
16
|
+
|
17
|
+
__all__ = [
|
18
|
+
"VMConfig",
|
19
|
+
"VMInfo",
|
20
|
+
"VMStatus",
|
21
|
+
"VMSize",
|
22
|
+
"VMResources",
|
23
|
+
"SSHKey",
|
24
|
+
"VMProvider",
|
25
|
+
"MultipassProvider",
|
26
|
+
"VMError",
|
27
|
+
"VMCreateError",
|
28
|
+
"VMNotFoundError",
|
29
|
+
"VMStateError",
|
30
|
+
"ResourceError"
|
31
|
+
]
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import yaml
|
2
|
+
import tempfile
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Dict, Optional
|
5
|
+
|
6
|
+
def generate_cloud_init(
|
7
|
+
hostname: str,
|
8
|
+
ssh_key: str,
|
9
|
+
packages: Optional[list[str]] = None,
|
10
|
+
runcmd: Optional[list[str]] = None
|
11
|
+
) -> str:
|
12
|
+
"""Generate cloud-init configuration.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
hostname: VM hostname
|
16
|
+
ssh_key: SSH public key to add to authorized_keys
|
17
|
+
packages: List of packages to install
|
18
|
+
runcmd: List of commands to run on first boot
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
Path to cloud-init configuration file
|
22
|
+
"""
|
23
|
+
config = {
|
24
|
+
"hostname": hostname,
|
25
|
+
"package_update": True,
|
26
|
+
"package_upgrade": True,
|
27
|
+
"ssh_authorized_keys": [ssh_key],
|
28
|
+
"users": [{
|
29
|
+
"name": "root",
|
30
|
+
"ssh_authorized_keys": [ssh_key]
|
31
|
+
}],
|
32
|
+
"write_files": [
|
33
|
+
{
|
34
|
+
"path": "/etc/ssh/sshd_config.d/allow_root.conf",
|
35
|
+
"content": "PermitRootLogin prohibit-password\n",
|
36
|
+
"owner": "root:root",
|
37
|
+
"permissions": "0644"
|
38
|
+
}
|
39
|
+
],
|
40
|
+
"runcmd": [
|
41
|
+
"systemctl restart ssh"
|
42
|
+
]
|
43
|
+
}
|
44
|
+
|
45
|
+
if packages:
|
46
|
+
config["packages"] = packages
|
47
|
+
|
48
|
+
if runcmd:
|
49
|
+
config["runcmd"].extend(runcmd)
|
50
|
+
|
51
|
+
# Create temporary file
|
52
|
+
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False)
|
53
|
+
yaml.safe_dump(config, temp_file)
|
54
|
+
temp_file.close()
|
55
|
+
|
56
|
+
return temp_file.name
|
57
|
+
|
58
|
+
def cleanup_cloud_init(path: str) -> None:
|
59
|
+
"""Clean up cloud-init configuration file.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
path: Path to cloud-init configuration file
|
63
|
+
"""
|
64
|
+
try:
|
65
|
+
Path(path).unlink()
|
66
|
+
except Exception:
|
67
|
+
pass
|
provider/vm/models.py
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from pydantic import BaseModel, Field, validator
|
3
|
+
from typing import Dict, Optional
|
4
|
+
from datetime import datetime
|
5
|
+
|
6
|
+
|
7
|
+
class VMStatus(str, Enum):
|
8
|
+
"""VM status enum."""
|
9
|
+
CREATING = "creating"
|
10
|
+
RUNNING = "running"
|
11
|
+
STOPPING = "stopping"
|
12
|
+
STOPPED = "stopped"
|
13
|
+
ERROR = "error"
|
14
|
+
DELETED = "deleted"
|
15
|
+
|
16
|
+
|
17
|
+
class VMSize(str, Enum):
|
18
|
+
"""Predefined VM sizes."""
|
19
|
+
SMALL = "small" # 1 CPU, 1GB RAM, 10GB storage
|
20
|
+
MEDIUM = "medium" # 2 CPU, 4GB RAM, 20GB storage
|
21
|
+
LARGE = "large" # 4 CPU, 8GB RAM, 40GB storage
|
22
|
+
XLARGE = "xlarge" # 8 CPU, 16GB RAM, 80GB storage
|
23
|
+
|
24
|
+
|
25
|
+
class VMResources(BaseModel):
|
26
|
+
"""VM resource configuration."""
|
27
|
+
cpu: int = Field(..., ge=1, description="Number of CPU cores")
|
28
|
+
memory: int = Field(..., ge=1, description="Memory in GB")
|
29
|
+
storage: int = Field(..., ge=10, description="Storage in GB")
|
30
|
+
|
31
|
+
@validator("cpu")
|
32
|
+
def validate_cpu(cls, v: int) -> int:
|
33
|
+
"""Validate CPU cores."""
|
34
|
+
if v not in [1, 2, 4, 8, 16]:
|
35
|
+
raise ValueError("CPU cores must be 1, 2, 4, 8, or 16")
|
36
|
+
return v
|
37
|
+
|
38
|
+
@validator("memory")
|
39
|
+
def validate_memory(cls, v: int) -> int:
|
40
|
+
"""Validate memory."""
|
41
|
+
if v not in [1, 2, 4, 8, 16, 32, 64]:
|
42
|
+
raise ValueError("Memory must be 1, 2, 4, 8, 16, 32, or 64 GB")
|
43
|
+
return v
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
def from_size(cls, size: VMSize) -> "VMResources":
|
47
|
+
"""Create resources from predefined size."""
|
48
|
+
sizes = {
|
49
|
+
VMSize.SMALL: {"cpu": 1, "memory": 1, "storage": 10},
|
50
|
+
VMSize.MEDIUM: {"cpu": 2, "memory": 4, "storage": 20},
|
51
|
+
VMSize.LARGE: {"cpu": 4, "memory": 8, "storage": 40},
|
52
|
+
VMSize.XLARGE: {"cpu": 8, "memory": 16, "storage": 80}
|
53
|
+
}
|
54
|
+
return cls(**sizes[size])
|
55
|
+
|
56
|
+
|
57
|
+
class VMCreateRequest(BaseModel):
|
58
|
+
"""Request to create a new VM."""
|
59
|
+
name: str = Field(..., min_length=3, max_length=64,
|
60
|
+
regex="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
|
61
|
+
size: Optional[VMSize] = None
|
62
|
+
cpu_cores: Optional[int] = None
|
63
|
+
memory_gb: Optional[int] = None
|
64
|
+
storage_gb: Optional[int] = None
|
65
|
+
image: Optional[str] = Field(default="24.04") # Ubuntu 24.04 LTS
|
66
|
+
ssh_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ",
|
67
|
+
description="SSH public key for VM access")
|
68
|
+
|
69
|
+
@validator("name")
|
70
|
+
def validate_name(cls, v: str) -> str:
|
71
|
+
"""Validate VM name."""
|
72
|
+
if "--" in v:
|
73
|
+
raise ValueError("VM name cannot contain consecutive hyphens")
|
74
|
+
return v
|
75
|
+
|
76
|
+
@validator("cpu_cores")
|
77
|
+
def validate_cpu(cls, v: Optional[int]) -> Optional[int]:
|
78
|
+
"""Validate CPU cores."""
|
79
|
+
if v is not None and v not in [1, 2, 4, 8, 16]:
|
80
|
+
raise ValueError("CPU cores must be 1, 2, 4, 8, or 16")
|
81
|
+
return v
|
82
|
+
|
83
|
+
@validator("memory_gb")
|
84
|
+
def validate_memory(cls, v: Optional[int]) -> Optional[int]:
|
85
|
+
"""Validate memory."""
|
86
|
+
if v is not None and v not in [1, 2, 4, 8, 16, 32, 64]:
|
87
|
+
raise ValueError("Memory must be 1, 2, 4, 8, 16, 32, or 64 GB")
|
88
|
+
return v
|
89
|
+
|
90
|
+
|
91
|
+
class VMConfig(BaseModel):
|
92
|
+
"""VM configuration."""
|
93
|
+
name: str = Field(..., min_length=3, max_length=64,
|
94
|
+
regex="^[a-z0-9][a-z0-9-]*[a-z0-9]$")
|
95
|
+
resources: VMResources
|
96
|
+
image: str = Field(default="24.04") # Ubuntu 24.04 LTS
|
97
|
+
size: Optional[VMSize] = None
|
98
|
+
ssh_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ",
|
99
|
+
description="SSH public key for VM access")
|
100
|
+
|
101
|
+
@validator("name")
|
102
|
+
def validate_name(cls, v: str) -> str:
|
103
|
+
"""Validate VM name."""
|
104
|
+
if "--" in v:
|
105
|
+
raise ValueError("VM name cannot contain consecutive hyphens")
|
106
|
+
return v
|
107
|
+
|
108
|
+
|
109
|
+
class VMInfo(BaseModel):
|
110
|
+
"""VM information."""
|
111
|
+
id: str
|
112
|
+
name: str
|
113
|
+
status: VMStatus
|
114
|
+
resources: VMResources
|
115
|
+
ip_address: Optional[str] = None
|
116
|
+
ssh_port: Optional[int] = None
|
117
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
118
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
119
|
+
error_message: Optional[str] = None
|
120
|
+
|
121
|
+
class Config:
|
122
|
+
json_encoders = {
|
123
|
+
datetime: lambda v: v.isoformat()
|
124
|
+
}
|
125
|
+
|
126
|
+
|
127
|
+
class SSHKey(BaseModel):
|
128
|
+
"""SSH key information."""
|
129
|
+
name: str = Field(..., min_length=1, max_length=64)
|
130
|
+
public_key: str = Field(..., regex="^(ssh-rsa|ssh-ed25519) ")
|
131
|
+
fingerprint: Optional[str] = None
|
132
|
+
|
133
|
+
|
134
|
+
class VMAccessInfo(BaseModel):
|
135
|
+
"""VM access information."""
|
136
|
+
ssh_host: str
|
137
|
+
ssh_port: int
|
138
|
+
vm_id: str = Field(..., description="Requestor's VM name")
|
139
|
+
multipass_name: str = Field(...,
|
140
|
+
description="Full multipass VM name with timestamp")
|
141
|
+
|
142
|
+
|
143
|
+
class VMProvider:
|
144
|
+
"""Base interface for VM providers."""
|
145
|
+
|
146
|
+
async def initialize(self) -> None:
|
147
|
+
"""Initialize the provider."""
|
148
|
+
raise NotImplementedError()
|
149
|
+
|
150
|
+
async def cleanup(self) -> None:
|
151
|
+
"""Cleanup provider resources."""
|
152
|
+
raise NotImplementedError()
|
153
|
+
|
154
|
+
async def create_vm(self, config: VMConfig) -> VMInfo:
|
155
|
+
"""Create a new VM."""
|
156
|
+
raise NotImplementedError()
|
157
|
+
|
158
|
+
async def delete_vm(self, vm_id: str) -> None:
|
159
|
+
"""Delete a VM."""
|
160
|
+
raise NotImplementedError()
|
161
|
+
|
162
|
+
async def start_vm(self, vm_id: str) -> VMInfo:
|
163
|
+
"""Start a VM."""
|
164
|
+
raise NotImplementedError()
|
165
|
+
|
166
|
+
async def stop_vm(self, vm_id: str) -> VMInfo:
|
167
|
+
"""Stop a VM."""
|
168
|
+
raise NotImplementedError()
|
169
|
+
|
170
|
+
async def get_vm_status(self, vm_id: str) -> VMInfo:
|
171
|
+
"""Get VM status."""
|
172
|
+
raise NotImplementedError()
|
173
|
+
|
174
|
+
async def add_ssh_key(self, vm_id: str, key: SSHKey) -> None:
|
175
|
+
"""Add SSH key to VM."""
|
176
|
+
raise NotImplementedError()
|
177
|
+
|
178
|
+
|
179
|
+
class VMError(Exception):
|
180
|
+
"""Base class for VM errors."""
|
181
|
+
|
182
|
+
def __init__(self, message: str, vm_id: Optional[str] = None):
|
183
|
+
self.message = message
|
184
|
+
self.vm_id = vm_id
|
185
|
+
super().__init__(message)
|
186
|
+
|
187
|
+
|
188
|
+
class VMCreateError(VMError):
|
189
|
+
"""Error creating VM."""
|
190
|
+
pass
|
191
|
+
|
192
|
+
|
193
|
+
class VMNotFoundError(VMError):
|
194
|
+
"""VM not found."""
|
195
|
+
pass
|
196
|
+
|
197
|
+
|
198
|
+
class VMStateError(VMError):
|
199
|
+
"""Invalid VM state for operation."""
|
200
|
+
pass
|
201
|
+
|
202
|
+
|
203
|
+
class ResourceError(VMError):
|
204
|
+
"""Resource allocation error."""
|
205
|
+
pass
|