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,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")
@@ -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
@@ -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