vm-tool 1.0.32__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.
- examples/README.md +5 -0
- examples/__init__.py +1 -0
- examples/cloud/README.md +3 -0
- examples/cloud/__init__.py +1 -0
- examples/cloud/ssh_identity_file.py +27 -0
- examples/cloud/ssh_password.py +27 -0
- examples/cloud/template_cloud_setup.py +36 -0
- examples/deploy_full_setup.py +44 -0
- examples/docker-compose.example.yml +47 -0
- examples/ec2-setup.sh +95 -0
- examples/github-actions-ec2.yml +245 -0
- examples/github-actions-full-setup.yml +58 -0
- examples/local/.keep +1 -0
- examples/local/README.md +3 -0
- examples/local/__init__.py +1 -0
- examples/local/template_local_setup.py +27 -0
- examples/production-deploy.sh +70 -0
- examples/rollback.sh +52 -0
- examples/setup.sh +52 -0
- examples/ssh_key_management.py +22 -0
- examples/version_check.sh +3 -0
- vm_tool/__init__.py +0 -0
- vm_tool/alerting.py +274 -0
- vm_tool/audit.py +118 -0
- vm_tool/backup.py +125 -0
- vm_tool/benchmarking.py +200 -0
- vm_tool/cli.py +761 -0
- vm_tool/cloud.py +125 -0
- vm_tool/completion.py +200 -0
- vm_tool/compliance.py +104 -0
- vm_tool/config.py +92 -0
- vm_tool/drift.py +98 -0
- vm_tool/generator.py +462 -0
- vm_tool/health.py +197 -0
- vm_tool/history.py +131 -0
- vm_tool/kubernetes.py +89 -0
- vm_tool/metrics.py +183 -0
- vm_tool/notifications.py +152 -0
- vm_tool/plugins.py +119 -0
- vm_tool/policy.py +197 -0
- vm_tool/rbac.py +140 -0
- vm_tool/recovery.py +169 -0
- vm_tool/reporting.py +218 -0
- vm_tool/runner.py +445 -0
- vm_tool/secrets.py +285 -0
- vm_tool/ssh.py +150 -0
- vm_tool/state.py +122 -0
- vm_tool/strategies/__init__.py +16 -0
- vm_tool/strategies/ab_testing.py +258 -0
- vm_tool/strategies/blue_green.py +227 -0
- vm_tool/strategies/canary.py +277 -0
- vm_tool/validation.py +267 -0
- vm_tool/vm_setup/cleanup.yml +27 -0
- vm_tool/vm_setup/docker/create_docker_service.yml +63 -0
- vm_tool/vm_setup/docker/docker_setup.yml +7 -0
- vm_tool/vm_setup/docker/install_docker_and_compose.yml +92 -0
- vm_tool/vm_setup/docker/login_to_docker_hub.yml +6 -0
- vm_tool/vm_setup/github/git_configuration.yml +68 -0
- vm_tool/vm_setup/inventory.yml +1 -0
- vm_tool/vm_setup/k8s.yml +15 -0
- vm_tool/vm_setup/main.yml +27 -0
- vm_tool/vm_setup/monitoring.yml +42 -0
- vm_tool/vm_setup/project_service.yml +17 -0
- vm_tool/vm_setup/push_code.yml +40 -0
- vm_tool/vm_setup/setup.yml +17 -0
- vm_tool/vm_setup/setup_project_env.yml +7 -0
- vm_tool/webhooks.py +83 -0
- vm_tool-1.0.32.dist-info/METADATA +213 -0
- vm_tool-1.0.32.dist-info/RECORD +73 -0
- vm_tool-1.0.32.dist-info/WHEEL +5 -0
- vm_tool-1.0.32.dist-info/entry_points.txt +2 -0
- vm_tool-1.0.32.dist-info/licenses/LICENSE +21 -0
- vm_tool-1.0.32.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Canary deployment strategy with gradual rollout and automatic rollback."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional, Dict, Any, List
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CanaryConfig:
|
|
13
|
+
"""Configuration for canary deployment."""
|
|
14
|
+
|
|
15
|
+
initial_percentage: int = 10 # Start with 10% traffic
|
|
16
|
+
increment_percentage: int = 10 # Increase by 10% each step
|
|
17
|
+
increment_interval: int = 300 # Wait 5 minutes between increments
|
|
18
|
+
success_threshold: float = 99.0 # 99% success rate required
|
|
19
|
+
error_threshold: float = 1.0 # Max 1% error rate
|
|
20
|
+
rollback_on_failure: bool = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CanaryDeployment:
|
|
24
|
+
"""Manage canary deployments with gradual traffic shifting."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
production_host: str,
|
|
29
|
+
canary_host: str,
|
|
30
|
+
config: Optional[CanaryConfig] = None,
|
|
31
|
+
):
|
|
32
|
+
self.production_host = production_host
|
|
33
|
+
self.canary_host = canary_host
|
|
34
|
+
self.config = config or CanaryConfig()
|
|
35
|
+
self.current_percentage = 0
|
|
36
|
+
|
|
37
|
+
def deploy(
|
|
38
|
+
self,
|
|
39
|
+
compose_file: str,
|
|
40
|
+
monitor_metrics: bool = True,
|
|
41
|
+
) -> Dict[str, Any]:
|
|
42
|
+
"""Execute canary deployment with gradual rollout.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
compose_file: Docker compose file to deploy
|
|
46
|
+
monitor_metrics: Whether to monitor metrics during rollout
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Deployment result
|
|
50
|
+
"""
|
|
51
|
+
logger.info("🐤 Starting canary deployment")
|
|
52
|
+
logger.info(f" Production: {self.production_host}")
|
|
53
|
+
logger.info(f" Canary: {self.canary_host}")
|
|
54
|
+
logger.info(f" Initial traffic: {self.config.initial_percentage}%")
|
|
55
|
+
|
|
56
|
+
result = {
|
|
57
|
+
"success": False,
|
|
58
|
+
"canary_host": self.canary_host,
|
|
59
|
+
"final_percentage": 0,
|
|
60
|
+
"steps": [],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Step 1: Deploy to canary environment
|
|
65
|
+
logger.info("📦 Deploying to canary environment...")
|
|
66
|
+
self._deploy_to_canary(compose_file)
|
|
67
|
+
|
|
68
|
+
# Step 2: Health check canary
|
|
69
|
+
logger.info("🏥 Running health checks on canary...")
|
|
70
|
+
if not self._health_check(self.canary_host):
|
|
71
|
+
raise Exception("Canary health check failed")
|
|
72
|
+
|
|
73
|
+
# Step 3: Gradual traffic shift
|
|
74
|
+
logger.info("🔄 Starting gradual traffic shift...")
|
|
75
|
+
self.current_percentage = self.config.initial_percentage
|
|
76
|
+
|
|
77
|
+
while self.current_percentage < 100:
|
|
78
|
+
logger.info(
|
|
79
|
+
f" Shifting {self.current_percentage}% traffic to canary..."
|
|
80
|
+
)
|
|
81
|
+
self._shift_traffic(self.current_percentage)
|
|
82
|
+
|
|
83
|
+
result["steps"].append(
|
|
84
|
+
{
|
|
85
|
+
"percentage": self.current_percentage,
|
|
86
|
+
"timestamp": time.time(),
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Monitor metrics if enabled
|
|
91
|
+
if monitor_metrics and self.current_percentage < 100:
|
|
92
|
+
logger.info(
|
|
93
|
+
f" Monitoring metrics for {self.config.increment_interval}s..."
|
|
94
|
+
)
|
|
95
|
+
time.sleep(self.config.increment_interval)
|
|
96
|
+
|
|
97
|
+
metrics = self._get_canary_metrics()
|
|
98
|
+
logger.info(f" Canary metrics: {metrics}")
|
|
99
|
+
|
|
100
|
+
# Check if metrics are acceptable
|
|
101
|
+
if not self._metrics_acceptable(metrics):
|
|
102
|
+
logger.error("❌ Canary metrics failed threshold")
|
|
103
|
+
if self.config.rollback_on_failure:
|
|
104
|
+
self._rollback()
|
|
105
|
+
raise Exception("Canary metrics below threshold")
|
|
106
|
+
|
|
107
|
+
# Increment traffic percentage
|
|
108
|
+
if self.current_percentage < 100:
|
|
109
|
+
self.current_percentage = min(
|
|
110
|
+
100, self.current_percentage + self.config.increment_percentage
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Step 4: Full cutover
|
|
114
|
+
logger.info("✅ Canary deployment successful - full cutover complete")
|
|
115
|
+
result["success"] = True
|
|
116
|
+
result["final_percentage"] = 100
|
|
117
|
+
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"❌ Canary deployment failed: {e}")
|
|
122
|
+
result["error"] = str(e)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def _deploy_to_canary(self, compose_file: str):
|
|
126
|
+
"""Deploy to canary environment."""
|
|
127
|
+
logger.info(f" Deploying {compose_file} to {self.canary_host}")
|
|
128
|
+
# TODO: Integrate with deploy-docker
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def _health_check(self, host: str) -> bool:
|
|
132
|
+
"""Check if host is healthy."""
|
|
133
|
+
from vm_tool.health import check_http
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
return check_http(f"http://{host}/health")
|
|
137
|
+
except:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def _shift_traffic(self, percentage: int):
|
|
141
|
+
"""Shift traffic to canary.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
percentage: Percentage of traffic to send to canary (0-100)
|
|
145
|
+
"""
|
|
146
|
+
# This would update load balancer configuration
|
|
147
|
+
# Implementation depends on LB type (nginx, AWS ALB, etc.)
|
|
148
|
+
logger.info(f" Traffic shift: {percentage}% to canary")
|
|
149
|
+
# TODO: Implement actual traffic shifting
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
def _get_canary_metrics(self) -> Dict[str, float]:
|
|
153
|
+
"""Get metrics from canary environment."""
|
|
154
|
+
# This would fetch actual metrics from monitoring system
|
|
155
|
+
# For now, return dummy metrics
|
|
156
|
+
return {
|
|
157
|
+
"success_rate": 99.5,
|
|
158
|
+
"error_rate": 0.5,
|
|
159
|
+
"avg_response_time": 120,
|
|
160
|
+
"requests_per_second": 100,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def _metrics_acceptable(self, metrics: Dict[str, float]) -> bool:
|
|
164
|
+
"""Check if metrics meet thresholds."""
|
|
165
|
+
success_rate = metrics.get("success_rate", 0)
|
|
166
|
+
error_rate = metrics.get("error_rate", 100)
|
|
167
|
+
|
|
168
|
+
if success_rate < self.config.success_threshold:
|
|
169
|
+
logger.warning(
|
|
170
|
+
f" Success rate {success_rate}% below threshold {self.config.success_threshold}%"
|
|
171
|
+
)
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
if error_rate > self.config.error_threshold:
|
|
175
|
+
logger.warning(
|
|
176
|
+
f" Error rate {error_rate}% above threshold {self.config.error_threshold}%"
|
|
177
|
+
)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def _rollback(self):
|
|
183
|
+
"""Rollback canary deployment."""
|
|
184
|
+
logger.info("🔄 Rolling back canary deployment...")
|
|
185
|
+
self._shift_traffic(0) # Send all traffic back to production
|
|
186
|
+
logger.info("✅ Rollback complete")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ProgressiveRollout:
|
|
190
|
+
"""Progressive rollout across multiple hosts."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, hosts: List[str], batch_size: int = 1):
|
|
193
|
+
self.hosts = hosts
|
|
194
|
+
self.batch_size = batch_size
|
|
195
|
+
self.deployed_hosts: List[str] = []
|
|
196
|
+
|
|
197
|
+
def deploy(
|
|
198
|
+
self,
|
|
199
|
+
compose_file: str,
|
|
200
|
+
wait_between_batches: int = 60,
|
|
201
|
+
) -> Dict[str, Any]:
|
|
202
|
+
"""Deploy progressively across hosts.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
compose_file: Docker compose file
|
|
206
|
+
wait_between_batches: Seconds to wait between batches
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Deployment result
|
|
210
|
+
"""
|
|
211
|
+
logger.info(f"🚀 Starting progressive rollout to {len(self.hosts)} hosts")
|
|
212
|
+
logger.info(f" Batch size: {self.batch_size}")
|
|
213
|
+
|
|
214
|
+
result = {
|
|
215
|
+
"success": False,
|
|
216
|
+
"deployed_hosts": [],
|
|
217
|
+
"failed_hosts": [],
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Deploy in batches
|
|
221
|
+
for i in range(0, len(self.hosts), self.batch_size):
|
|
222
|
+
batch = self.hosts[i : i + self.batch_size]
|
|
223
|
+
batch_num = (i // self.batch_size) + 1
|
|
224
|
+
total_batches = (len(self.hosts) + self.batch_size - 1) // self.batch_size
|
|
225
|
+
|
|
226
|
+
logger.info(f"📦 Deploying batch {batch_num}/{total_batches}: {batch}")
|
|
227
|
+
|
|
228
|
+
for host in batch:
|
|
229
|
+
try:
|
|
230
|
+
self._deploy_to_host(host, compose_file)
|
|
231
|
+
|
|
232
|
+
# Health check
|
|
233
|
+
if self._health_check(host):
|
|
234
|
+
self.deployed_hosts.append(host)
|
|
235
|
+
result["deployed_hosts"].append(host)
|
|
236
|
+
logger.info(f" ✅ {host} deployed successfully")
|
|
237
|
+
else:
|
|
238
|
+
raise Exception("Health check failed")
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f" ❌ {host} deployment failed: {e}")
|
|
242
|
+
result["failed_hosts"].append(host)
|
|
243
|
+
# Optionally stop on first failure
|
|
244
|
+
# raise
|
|
245
|
+
|
|
246
|
+
# Wait before next batch (unless this is the last batch)
|
|
247
|
+
if i + self.batch_size < len(self.hosts):
|
|
248
|
+
logger.info(f"⏳ Waiting {wait_between_batches}s before next batch...")
|
|
249
|
+
time.sleep(wait_between_batches)
|
|
250
|
+
|
|
251
|
+
result["success"] = len(result["failed_hosts"]) == 0
|
|
252
|
+
|
|
253
|
+
if result["success"]:
|
|
254
|
+
logger.info(
|
|
255
|
+
f"✅ Progressive rollout complete: {len(self.deployed_hosts)} hosts"
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
logger.warning(
|
|
259
|
+
f"⚠️ Rollout completed with failures: {len(result['failed_hosts'])} failed"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return result
|
|
263
|
+
|
|
264
|
+
def _deploy_to_host(self, host: str, compose_file: str):
|
|
265
|
+
"""Deploy to a single host."""
|
|
266
|
+
logger.info(f" Deploying to {host}...")
|
|
267
|
+
# TODO: Integrate with deploy-docker
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
def _health_check(self, host: str) -> bool:
|
|
271
|
+
"""Check if host is healthy."""
|
|
272
|
+
from vm_tool.health import check_http
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
return check_http(f"http://{host}/health")
|
|
276
|
+
except:
|
|
277
|
+
return False
|
vm_tool/validation.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Deployment validation framework."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Dict, Any, Callable, Optional
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ValidationStatus(Enum):
|
|
12
|
+
"""Validation check status."""
|
|
13
|
+
|
|
14
|
+
PASSED = "passed"
|
|
15
|
+
FAILED = "failed"
|
|
16
|
+
SKIPPED = "skipped"
|
|
17
|
+
WARNING = "warning"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ValidationResult:
|
|
22
|
+
"""Result of a validation check."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
status: ValidationStatus
|
|
26
|
+
message: str
|
|
27
|
+
details: Optional[Dict[str, Any]] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DeploymentValidator:
|
|
31
|
+
"""Validate deployment readiness and health."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, host: str, port: int = 8000):
|
|
34
|
+
self.host = host
|
|
35
|
+
self.port = port
|
|
36
|
+
self.checks: List[Callable] = []
|
|
37
|
+
self.results: List[ValidationResult] = []
|
|
38
|
+
|
|
39
|
+
def add_check(self, check_func: Callable, name: str):
|
|
40
|
+
"""Add a validation check."""
|
|
41
|
+
self.checks.append((name, check_func))
|
|
42
|
+
|
|
43
|
+
def validate_all(self) -> bool:
|
|
44
|
+
"""Run all validation checks.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if all checks passed, False otherwise
|
|
48
|
+
"""
|
|
49
|
+
self.results = []
|
|
50
|
+
all_passed = True
|
|
51
|
+
|
|
52
|
+
logger.info(f"🔍 Running {len(self.checks)} validation checks...")
|
|
53
|
+
|
|
54
|
+
for name, check_func in self.checks:
|
|
55
|
+
try:
|
|
56
|
+
result = check_func()
|
|
57
|
+
self.results.append(result)
|
|
58
|
+
|
|
59
|
+
if result.status == ValidationStatus.PASSED:
|
|
60
|
+
logger.info(f" ✅ {name}: {result.message}")
|
|
61
|
+
elif result.status == ValidationStatus.WARNING:
|
|
62
|
+
logger.warning(f" ⚠️ {name}: {result.message}")
|
|
63
|
+
elif result.status == ValidationStatus.FAILED:
|
|
64
|
+
logger.error(f" ❌ {name}: {result.message}")
|
|
65
|
+
all_passed = False
|
|
66
|
+
else: # SKIPPED
|
|
67
|
+
logger.info(f" ⏭️ {name}: {result.message}")
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f" ❌ {name}: Check failed with error: {e}")
|
|
71
|
+
self.results.append(
|
|
72
|
+
ValidationResult(
|
|
73
|
+
name=name,
|
|
74
|
+
status=ValidationStatus.FAILED,
|
|
75
|
+
message=f"Check failed: {e}",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
all_passed = False
|
|
79
|
+
|
|
80
|
+
return all_passed
|
|
81
|
+
|
|
82
|
+
def check_port_open(self) -> ValidationResult:
|
|
83
|
+
"""Check if application port is open."""
|
|
84
|
+
import socket
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
88
|
+
sock.settimeout(5)
|
|
89
|
+
result = sock.connect_ex((self.host, self.port))
|
|
90
|
+
sock.close()
|
|
91
|
+
|
|
92
|
+
if result == 0:
|
|
93
|
+
return ValidationResult(
|
|
94
|
+
name="Port Check",
|
|
95
|
+
status=ValidationStatus.PASSED,
|
|
96
|
+
message=f"Port {self.port} is open",
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
return ValidationResult(
|
|
100
|
+
name="Port Check",
|
|
101
|
+
status=ValidationStatus.FAILED,
|
|
102
|
+
message=f"Port {self.port} is not accessible",
|
|
103
|
+
)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return ValidationResult(
|
|
106
|
+
name="Port Check",
|
|
107
|
+
status=ValidationStatus.FAILED,
|
|
108
|
+
message=f"Port check failed: {e}",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def check_http_response(self, endpoint: str = "/") -> ValidationResult:
|
|
112
|
+
"""Check if HTTP endpoint responds."""
|
|
113
|
+
import requests
|
|
114
|
+
|
|
115
|
+
url = f"http://{self.host}:{self.port}{endpoint}"
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = requests.get(url, timeout=10)
|
|
119
|
+
|
|
120
|
+
if response.status_code == 200:
|
|
121
|
+
return ValidationResult(
|
|
122
|
+
name="HTTP Response",
|
|
123
|
+
status=ValidationStatus.PASSED,
|
|
124
|
+
message=f"Endpoint {endpoint} returned 200",
|
|
125
|
+
details={"status_code": response.status_code},
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
return ValidationResult(
|
|
129
|
+
name="HTTP Response",
|
|
130
|
+
status=ValidationStatus.WARNING,
|
|
131
|
+
message=f"Endpoint {endpoint} returned {response.status_code}",
|
|
132
|
+
details={"status_code": response.status_code},
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
return ValidationResult(
|
|
136
|
+
name="HTTP Response",
|
|
137
|
+
status=ValidationStatus.FAILED,
|
|
138
|
+
message=f"HTTP check failed: {e}",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def check_health_endpoint(self, endpoint: str = "/health") -> ValidationResult:
|
|
142
|
+
"""Check health endpoint."""
|
|
143
|
+
import requests
|
|
144
|
+
|
|
145
|
+
url = f"http://{self.host}:{self.port}{endpoint}"
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
response = requests.get(url, timeout=10)
|
|
149
|
+
|
|
150
|
+
if response.status_code == 200:
|
|
151
|
+
try:
|
|
152
|
+
data = response.json()
|
|
153
|
+
status = data.get("status", "unknown")
|
|
154
|
+
|
|
155
|
+
if status == "healthy" or status == "ok":
|
|
156
|
+
return ValidationResult(
|
|
157
|
+
name="Health Check",
|
|
158
|
+
status=ValidationStatus.PASSED,
|
|
159
|
+
message="Application is healthy",
|
|
160
|
+
details=data,
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
return ValidationResult(
|
|
164
|
+
name="Health Check",
|
|
165
|
+
status=ValidationStatus.WARNING,
|
|
166
|
+
message=f"Health status: {status}",
|
|
167
|
+
details=data,
|
|
168
|
+
)
|
|
169
|
+
except:
|
|
170
|
+
return ValidationResult(
|
|
171
|
+
name="Health Check",
|
|
172
|
+
status=ValidationStatus.PASSED,
|
|
173
|
+
message="Health endpoint accessible",
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
return ValidationResult(
|
|
177
|
+
name="Health Check",
|
|
178
|
+
status=ValidationStatus.FAILED,
|
|
179
|
+
message=f"Health endpoint returned {response.status_code}",
|
|
180
|
+
)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return ValidationResult(
|
|
183
|
+
name="Health Check",
|
|
184
|
+
status=ValidationStatus.FAILED,
|
|
185
|
+
message=f"Health check failed: {e}",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def check_database_connection(self, db_url: str) -> ValidationResult:
|
|
189
|
+
"""Check database connectivity."""
|
|
190
|
+
# Placeholder - would implement actual DB connection check
|
|
191
|
+
return ValidationResult(
|
|
192
|
+
name="Database Connection",
|
|
193
|
+
status=ValidationStatus.SKIPPED,
|
|
194
|
+
message="Database check not configured",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def check_dependencies(self, dependencies: List[str]) -> ValidationResult:
|
|
198
|
+
"""Check if required dependencies are available."""
|
|
199
|
+
# Placeholder - would check service dependencies
|
|
200
|
+
return ValidationResult(
|
|
201
|
+
name="Dependencies Check",
|
|
202
|
+
status=ValidationStatus.SKIPPED,
|
|
203
|
+
message="Dependency check not configured",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def generate_report(self) -> str:
|
|
207
|
+
"""Generate validation report."""
|
|
208
|
+
passed = sum(1 for r in self.results if r.status == ValidationStatus.PASSED)
|
|
209
|
+
failed = sum(1 for r in self.results if r.status == ValidationStatus.FAILED)
|
|
210
|
+
warnings = sum(1 for r in self.results if r.status == ValidationStatus.WARNING)
|
|
211
|
+
skipped = sum(1 for r in self.results if r.status == ValidationStatus.SKIPPED)
|
|
212
|
+
|
|
213
|
+
report = f"""
|
|
214
|
+
Deployment Validation Report
|
|
215
|
+
============================
|
|
216
|
+
Host: {self.host}:{self.port}
|
|
217
|
+
Total Checks: {len(self.results)}
|
|
218
|
+
|
|
219
|
+
Results:
|
|
220
|
+
✅ Passed: {passed}
|
|
221
|
+
❌ Failed: {failed}
|
|
222
|
+
⚠️ Warnings: {warnings}
|
|
223
|
+
⏭️ Skipped: {skipped}
|
|
224
|
+
|
|
225
|
+
Details:
|
|
226
|
+
"""
|
|
227
|
+
for result in self.results:
|
|
228
|
+
icon = {
|
|
229
|
+
ValidationStatus.PASSED: "✅",
|
|
230
|
+
ValidationStatus.FAILED: "❌",
|
|
231
|
+
ValidationStatus.WARNING: "⚠️",
|
|
232
|
+
ValidationStatus.SKIPPED: "⏭️",
|
|
233
|
+
}[result.status]
|
|
234
|
+
|
|
235
|
+
report += f"\n{icon} {result.name}: {result.message}"
|
|
236
|
+
if result.details:
|
|
237
|
+
report += f"\n Details: {result.details}"
|
|
238
|
+
|
|
239
|
+
return report
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def validate_deployment(host: str, port: int = 8000) -> bool:
|
|
243
|
+
"""Quick deployment validation.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
host: Host to validate
|
|
247
|
+
port: Port to check
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if validation passed, False otherwise
|
|
251
|
+
"""
|
|
252
|
+
validator = DeploymentValidator(host, port)
|
|
253
|
+
|
|
254
|
+
# Add standard checks
|
|
255
|
+
validator.add_check(validator.check_port_open, "Port Accessibility")
|
|
256
|
+
validator.add_check(validator.check_http_response, "HTTP Response")
|
|
257
|
+
validator.add_check(
|
|
258
|
+
lambda: validator.check_health_endpoint("/health"), "Health Endpoint"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Run validation
|
|
262
|
+
result = validator.validate_all()
|
|
263
|
+
|
|
264
|
+
# Print report
|
|
265
|
+
print(validator.generate_report())
|
|
266
|
+
|
|
267
|
+
return result
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
- name: Cleanup VM
|
|
2
|
+
block:
|
|
3
|
+
- name: Remove GitHub credentials from git-credentials
|
|
4
|
+
file:
|
|
5
|
+
path: ~/.git-credentials
|
|
6
|
+
state: absent
|
|
7
|
+
become: yes
|
|
8
|
+
when: GITHUB_TOKEN is defined
|
|
9
|
+
|
|
10
|
+
- name: Clear Git credential helper configuration
|
|
11
|
+
shell: git config --global --unset credential.helper
|
|
12
|
+
become: yes
|
|
13
|
+
when: GITHUB_TOKEN is defined
|
|
14
|
+
|
|
15
|
+
- name: Unset environment variables
|
|
16
|
+
shell: |
|
|
17
|
+
unset GITHUB_PROJECT_URL
|
|
18
|
+
unset GITHUB_USERNAME
|
|
19
|
+
unset GITHUB_TOKEN
|
|
20
|
+
unset DOCKERHUB_USERNAME
|
|
21
|
+
unset DOCKERHUB_PASSWORD
|
|
22
|
+
environment:
|
|
23
|
+
GITHUB_PROJECT_URL: "{{ GITHUB_PROJECT_URL }}"
|
|
24
|
+
GITHUB_USERNAME: "{{ GITHUB_USERNAME }}"
|
|
25
|
+
GITHUB_TOKEN: "{{ GITHUB_TOKEN }}"
|
|
26
|
+
DOCKERHUB_USERNAME: "{{ DOCKERHUB_USERNAME }}"
|
|
27
|
+
DOCKERHUB_PASSWORD: "{{ DOCKERHUB_PASSWORD }}"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
- name: Clean up Docker Environment and Set up Docker Service
|
|
2
|
+
block:
|
|
3
|
+
- name: Check if Docker Service File Exists
|
|
4
|
+
ansible.builtin.stat:
|
|
5
|
+
path: "{{ service_file_path }}"
|
|
6
|
+
register: service_file_stat
|
|
7
|
+
|
|
8
|
+
- name: Stop project service and ignore errors
|
|
9
|
+
ansible.builtin.systemd:
|
|
10
|
+
name: project
|
|
11
|
+
state: stopped
|
|
12
|
+
ignore_errors: yes
|
|
13
|
+
|
|
14
|
+
# - name: Run Docker System Prune
|
|
15
|
+
# ansible.builtin.command:
|
|
16
|
+
# cmd: docker system prune -f
|
|
17
|
+
# ignore_errors: yes
|
|
18
|
+
|
|
19
|
+
- name: Remove Existing Docker Service File
|
|
20
|
+
ansible.builtin.file:
|
|
21
|
+
path: "{{ service_file_path }}"
|
|
22
|
+
state: absent
|
|
23
|
+
when: service_file_stat.stat.exists
|
|
24
|
+
|
|
25
|
+
- name: Create Docker Service File
|
|
26
|
+
ansible.builtin.file:
|
|
27
|
+
path: "{{ service_file_path }}"
|
|
28
|
+
state: touch
|
|
29
|
+
mode: '0644'
|
|
30
|
+
|
|
31
|
+
- name: Check if Docker Compose File Path is Valid
|
|
32
|
+
ansible.builtin.stat:
|
|
33
|
+
path: "{{ project_dest_dir }}/{{ DOCKER_COMPOSE_FILE_PATH | default('docker-compose.yml') }}"
|
|
34
|
+
register: compose_file_stat
|
|
35
|
+
|
|
36
|
+
- name: Fail if Docker Compose File Path is Invalid
|
|
37
|
+
ansible.builtin.fail:
|
|
38
|
+
msg: "The specified Docker Compose file path '{{ project_dest_dir }}/{{ DOCKER_COMPOSE_FILE_PATH | default('docker-compose.yml') }}' does not exist."
|
|
39
|
+
when: not compose_file_stat.stat.exists
|
|
40
|
+
|
|
41
|
+
- name: Insert Docker Service Configuration
|
|
42
|
+
ansible.builtin.blockinfile:
|
|
43
|
+
path: "{{ service_file_path }}"
|
|
44
|
+
block: |
|
|
45
|
+
[Unit]
|
|
46
|
+
Description=Project Docker Container
|
|
47
|
+
After=docker.service
|
|
48
|
+
|
|
49
|
+
[Service]
|
|
50
|
+
Type=simple
|
|
51
|
+
WorkingDirectory={{ project_dest_dir }}
|
|
52
|
+
ExecStart={{ DEPLOY_COMMAND if DEPLOY_COMMAND is defined else ('docker compose -f ' + (DOCKER_COMPOSE_FILE_PATH | default('docker-compose.yml')) + (' --env-file ' + ENV_FILE_PATH if ENV_FILE_PATH is defined else '') + ' up -d') }}
|
|
53
|
+
Restart=always
|
|
54
|
+
RestartSec=10s
|
|
55
|
+
|
|
56
|
+
[Install]
|
|
57
|
+
WantedBy=multi-user.target
|
|
58
|
+
|
|
59
|
+
- name: Reload Systemd Daemon
|
|
60
|
+
ansible.builtin.systemd:
|
|
61
|
+
daemon_reload: yes
|
|
62
|
+
|
|
63
|
+
become: yes
|