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.
Files changed (73) hide show
  1. examples/README.md +5 -0
  2. examples/__init__.py +1 -0
  3. examples/cloud/README.md +3 -0
  4. examples/cloud/__init__.py +1 -0
  5. examples/cloud/ssh_identity_file.py +27 -0
  6. examples/cloud/ssh_password.py +27 -0
  7. examples/cloud/template_cloud_setup.py +36 -0
  8. examples/deploy_full_setup.py +44 -0
  9. examples/docker-compose.example.yml +47 -0
  10. examples/ec2-setup.sh +95 -0
  11. examples/github-actions-ec2.yml +245 -0
  12. examples/github-actions-full-setup.yml +58 -0
  13. examples/local/.keep +1 -0
  14. examples/local/README.md +3 -0
  15. examples/local/__init__.py +1 -0
  16. examples/local/template_local_setup.py +27 -0
  17. examples/production-deploy.sh +70 -0
  18. examples/rollback.sh +52 -0
  19. examples/setup.sh +52 -0
  20. examples/ssh_key_management.py +22 -0
  21. examples/version_check.sh +3 -0
  22. vm_tool/__init__.py +0 -0
  23. vm_tool/alerting.py +274 -0
  24. vm_tool/audit.py +118 -0
  25. vm_tool/backup.py +125 -0
  26. vm_tool/benchmarking.py +200 -0
  27. vm_tool/cli.py +761 -0
  28. vm_tool/cloud.py +125 -0
  29. vm_tool/completion.py +200 -0
  30. vm_tool/compliance.py +104 -0
  31. vm_tool/config.py +92 -0
  32. vm_tool/drift.py +98 -0
  33. vm_tool/generator.py +462 -0
  34. vm_tool/health.py +197 -0
  35. vm_tool/history.py +131 -0
  36. vm_tool/kubernetes.py +89 -0
  37. vm_tool/metrics.py +183 -0
  38. vm_tool/notifications.py +152 -0
  39. vm_tool/plugins.py +119 -0
  40. vm_tool/policy.py +197 -0
  41. vm_tool/rbac.py +140 -0
  42. vm_tool/recovery.py +169 -0
  43. vm_tool/reporting.py +218 -0
  44. vm_tool/runner.py +445 -0
  45. vm_tool/secrets.py +285 -0
  46. vm_tool/ssh.py +150 -0
  47. vm_tool/state.py +122 -0
  48. vm_tool/strategies/__init__.py +16 -0
  49. vm_tool/strategies/ab_testing.py +258 -0
  50. vm_tool/strategies/blue_green.py +227 -0
  51. vm_tool/strategies/canary.py +277 -0
  52. vm_tool/validation.py +267 -0
  53. vm_tool/vm_setup/cleanup.yml +27 -0
  54. vm_tool/vm_setup/docker/create_docker_service.yml +63 -0
  55. vm_tool/vm_setup/docker/docker_setup.yml +7 -0
  56. vm_tool/vm_setup/docker/install_docker_and_compose.yml +92 -0
  57. vm_tool/vm_setup/docker/login_to_docker_hub.yml +6 -0
  58. vm_tool/vm_setup/github/git_configuration.yml +68 -0
  59. vm_tool/vm_setup/inventory.yml +1 -0
  60. vm_tool/vm_setup/k8s.yml +15 -0
  61. vm_tool/vm_setup/main.yml +27 -0
  62. vm_tool/vm_setup/monitoring.yml +42 -0
  63. vm_tool/vm_setup/project_service.yml +17 -0
  64. vm_tool/vm_setup/push_code.yml +40 -0
  65. vm_tool/vm_setup/setup.yml +17 -0
  66. vm_tool/vm_setup/setup_project_env.yml +7 -0
  67. vm_tool/webhooks.py +83 -0
  68. vm_tool-1.0.32.dist-info/METADATA +213 -0
  69. vm_tool-1.0.32.dist-info/RECORD +73 -0
  70. vm_tool-1.0.32.dist-info/WHEEL +5 -0
  71. vm_tool-1.0.32.dist-info/entry_points.txt +2 -0
  72. vm_tool-1.0.32.dist-info/licenses/LICENSE +21 -0
  73. vm_tool-1.0.32.dist-info/top_level.txt +2 -0
@@ -0,0 +1,258 @@
1
+ """A/B testing and traffic splitting strategies."""
2
+
3
+ import logging
4
+ from typing import Dict, Any, List, Optional
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class VariantType(Enum):
12
+ """A/B test variant type."""
13
+
14
+ CONTROL = "control" # Original version
15
+ VARIANT_A = "variant_a"
16
+ VARIANT_B = "variant_b"
17
+ VARIANT_C = "variant_c"
18
+
19
+
20
+ @dataclass
21
+ class Variant:
22
+ """A/B test variant configuration."""
23
+
24
+ name: str
25
+ host: str
26
+ traffic_percentage: float
27
+ description: Optional[str] = None
28
+
29
+
30
+ class ABTestDeployment:
31
+ """Manage A/B testing deployments."""
32
+
33
+ def __init__(self, variants: List[Variant]):
34
+ """Initialize A/B test deployment.
35
+
36
+ Args:
37
+ variants: List of test variants
38
+ """
39
+ self.variants = variants
40
+ self._validate_traffic_percentages()
41
+
42
+ def _validate_traffic_percentages(self):
43
+ """Validate that traffic percentages sum to 100."""
44
+ total = sum(v.traffic_percentage for v in self.variants)
45
+ if abs(total - 100.0) > 0.01: # Allow small floating point errors
46
+ raise ValueError(f"Traffic percentages must sum to 100%, got {total}%")
47
+
48
+ def deploy(self, compose_files: Dict[str, str]) -> Dict[str, Any]:
49
+ """Deploy A/B test variants.
50
+
51
+ Args:
52
+ compose_files: Mapping of variant name to compose file
53
+
54
+ Returns:
55
+ Deployment result
56
+ """
57
+ logger.info("πŸ§ͺ Starting A/B test deployment")
58
+ logger.info(f" Variants: {len(self.variants)}")
59
+
60
+ result = {
61
+ "success": False,
62
+ "deployed_variants": [],
63
+ "failed_variants": [],
64
+ }
65
+
66
+ # Deploy each variant
67
+ for variant in self.variants:
68
+ logger.info(
69
+ f"πŸ“¦ Deploying variant '{variant.name}' ({variant.traffic_percentage}%)"
70
+ )
71
+ logger.info(f" Host: {variant.host}")
72
+
73
+ compose_file = compose_files.get(variant.name)
74
+ if not compose_file:
75
+ logger.warning(
76
+ f" ⚠️ No compose file for variant '{variant.name}', skipping"
77
+ )
78
+ continue
79
+
80
+ try:
81
+ self._deploy_variant(variant, compose_file)
82
+ result["deployed_variants"].append(variant.name)
83
+ logger.info(f" βœ… Variant '{variant.name}' deployed")
84
+ except Exception as e:
85
+ logger.error(f" ❌ Variant '{variant.name}' failed: {e}")
86
+ result["failed_variants"].append(variant.name)
87
+
88
+ # Configure traffic splitting
89
+ if not result["failed_variants"]:
90
+ logger.info("πŸ”„ Configuring traffic splitting...")
91
+ self._configure_traffic_split()
92
+ result["success"] = True
93
+
94
+ return result
95
+
96
+ def _deploy_variant(self, variant: Variant, compose_file: str):
97
+ """Deploy a single variant."""
98
+ # TODO: Integrate with deploy-docker
99
+ logger.info(f" Deploying {compose_file} to {variant.host}")
100
+ pass
101
+
102
+ def _configure_traffic_split(self):
103
+ """Configure load balancer for traffic splitting."""
104
+ # This would configure the load balancer to split traffic
105
+ # based on variant percentages
106
+ for variant in self.variants:
107
+ logger.info(
108
+ f" {variant.name}: {variant.traffic_percentage}% β†’ {variant.host}"
109
+ )
110
+ # TODO: Implement actual LB configuration
111
+ pass
112
+
113
+ def get_variant_metrics(self, variant_name: str) -> Dict[str, Any]:
114
+ """Get metrics for a specific variant.
115
+
116
+ Args:
117
+ variant_name: Name of the variant
118
+
119
+ Returns:
120
+ Variant metrics
121
+ """
122
+ # This would fetch actual metrics from monitoring system
123
+ return {
124
+ "variant": variant_name,
125
+ "requests": 1000,
126
+ "success_rate": 99.5,
127
+ "avg_response_time": 120,
128
+ "conversion_rate": 5.2,
129
+ }
130
+
131
+ def compare_variants(self) -> Dict[str, Any]:
132
+ """Compare metrics across all variants.
133
+
134
+ Returns:
135
+ Comparison results
136
+ """
137
+ logger.info("πŸ“Š Comparing variant metrics...")
138
+
139
+ comparison = {
140
+ "variants": {},
141
+ "winner": None,
142
+ }
143
+
144
+ best_conversion = 0
145
+ best_variant = None
146
+
147
+ for variant in self.variants:
148
+ metrics = self.get_variant_metrics(variant.name)
149
+ comparison["variants"][variant.name] = metrics
150
+
151
+ conversion = metrics.get("conversion_rate", 0)
152
+ if conversion > best_conversion:
153
+ best_conversion = conversion
154
+ best_variant = variant.name
155
+
156
+ comparison["winner"] = best_variant
157
+
158
+ logger.info(f" Winner: {best_variant} ({best_conversion}% conversion)")
159
+
160
+ return comparison
161
+
162
+ def promote_winner(self, winner_name: str) -> bool:
163
+ """Promote winning variant to 100% traffic.
164
+
165
+ Args:
166
+ winner_name: Name of winning variant
167
+
168
+ Returns:
169
+ True if promotion successful
170
+ """
171
+ logger.info(f"πŸ† Promoting variant '{winner_name}' to 100% traffic")
172
+
173
+ # Update traffic percentages
174
+ for variant in self.variants:
175
+ if variant.name == winner_name:
176
+ variant.traffic_percentage = 100.0
177
+ else:
178
+ variant.traffic_percentage = 0.0
179
+
180
+ # Reconfigure traffic split
181
+ self._configure_traffic_split()
182
+
183
+ logger.info("βœ… Winner promoted successfully")
184
+ return True
185
+
186
+
187
+ class TrafficSplitter:
188
+ """Manage traffic splitting across multiple backends."""
189
+
190
+ def __init__(self, backends: Dict[str, str]):
191
+ """Initialize traffic splitter.
192
+
193
+ Args:
194
+ backends: Mapping of backend name to host
195
+ """
196
+ self.backends = backends
197
+ self.traffic_weights: Dict[str, float] = {}
198
+
199
+ def set_weights(self, weights: Dict[str, float]):
200
+ """Set traffic weights for backends.
201
+
202
+ Args:
203
+ weights: Mapping of backend name to weight (0-100)
204
+ """
205
+ total = sum(weights.values())
206
+ if abs(total - 100.0) > 0.01:
207
+ raise ValueError(f"Weights must sum to 100%, got {total}%")
208
+
209
+ self.traffic_weights = weights
210
+ self._apply_weights()
211
+
212
+ def _apply_weights(self):
213
+ """Apply traffic weights to load balancer."""
214
+ logger.info("πŸ”„ Applying traffic weights:")
215
+ for backend, weight in self.traffic_weights.items():
216
+ host = self.backends.get(backend, "unknown")
217
+ logger.info(f" {backend}: {weight}% β†’ {host}")
218
+
219
+ # TODO: Implement actual LB weight configuration
220
+ pass
221
+
222
+ def gradual_shift(
223
+ self,
224
+ from_backend: str,
225
+ to_backend: str,
226
+ increment: float = 10.0,
227
+ interval: int = 60,
228
+ ):
229
+ """Gradually shift traffic from one backend to another.
230
+
231
+ Args:
232
+ from_backend: Source backend
233
+ to_backend: Target backend
234
+ increment: Percentage to shift per step
235
+ interval: Seconds between shifts
236
+ """
237
+ logger.info(f"πŸ”„ Gradual traffic shift: {from_backend} β†’ {to_backend}")
238
+
239
+ current_from = self.traffic_weights.get(from_backend, 100.0)
240
+ current_to = self.traffic_weights.get(to_backend, 0.0)
241
+
242
+ import time
243
+
244
+ while current_from > 0:
245
+ shift_amount = min(increment, current_from)
246
+ current_from -= shift_amount
247
+ current_to += shift_amount
248
+
249
+ self.traffic_weights[from_backend] = current_from
250
+ self.traffic_weights[to_backend] = current_to
251
+
252
+ self._apply_weights()
253
+
254
+ if current_from > 0:
255
+ logger.info(f" Waiting {interval}s before next shift...")
256
+ time.sleep(interval)
257
+
258
+ logger.info("βœ… Traffic shift complete")
@@ -0,0 +1,227 @@
1
+ """Blue-Green deployment strategy for zero-downtime deployments."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Optional, Dict, Any
6
+ from pathlib import Path
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class BlueGreenDeployment:
12
+ """Manage blue-green deployments for zero-downtime updates."""
13
+
14
+ def __init__(
15
+ self,
16
+ blue_host: str,
17
+ green_host: str,
18
+ load_balancer_host: Optional[str] = None,
19
+ health_check_url: str = "/health",
20
+ health_check_timeout: int = 300,
21
+ ):
22
+ """Initialize blue-green deployment.
23
+
24
+ Args:
25
+ blue_host: Blue environment host
26
+ green_host: Green environment host
27
+ load_balancer_host: Load balancer host (if using external LB)
28
+ health_check_url: Health check endpoint
29
+ health_check_timeout: Max time to wait for health checks (seconds)
30
+ """
31
+ self.blue_host = blue_host
32
+ self.green_host = green_host
33
+ self.load_balancer_host = load_balancer_host
34
+ self.health_check_url = health_check_url
35
+ self.health_check_timeout = health_check_timeout
36
+ self.current_active = "blue" # Track which environment is active
37
+
38
+ def deploy(
39
+ self,
40
+ compose_file: str,
41
+ target_env: Optional[str] = None,
42
+ auto_switch: bool = True,
43
+ ) -> Dict[str, Any]:
44
+ """Execute blue-green deployment.
45
+
46
+ Args:
47
+ compose_file: Path to docker-compose file
48
+ target_env: Target environment ('blue' or 'green'), auto-detect if None
49
+ auto_switch: Automatically switch traffic after successful deployment
50
+
51
+ Returns:
52
+ Deployment result with status and details
53
+ """
54
+ # Determine target environment (deploy to inactive)
55
+ if target_env is None:
56
+ target_env = "green" if self.current_active == "blue" else "blue"
57
+
58
+ target_host = self.green_host if target_env == "green" else self.blue_host
59
+
60
+ logger.info(f"πŸ”΅πŸŸ’ Starting blue-green deployment to {target_env} environment")
61
+ logger.info(f" Target: {target_host}")
62
+ logger.info(f" Current active: {self.current_active}")
63
+
64
+ result = {
65
+ "success": False,
66
+ "target_env": target_env,
67
+ "target_host": target_host,
68
+ "previous_active": self.current_active,
69
+ "switched": False,
70
+ }
71
+
72
+ try:
73
+ # Step 1: Deploy to target environment
74
+ logger.info(f"πŸ“¦ Deploying to {target_env} environment...")
75
+ self._deploy_to_host(target_host, compose_file)
76
+
77
+ # Step 2: Run health checks
78
+ logger.info(f"πŸ₯ Running health checks on {target_env}...")
79
+ if not self._wait_for_health(target_host):
80
+ raise Exception(f"Health checks failed on {target_env} environment")
81
+
82
+ logger.info(f"βœ… {target_env.capitalize()} environment is healthy")
83
+
84
+ # Step 3: Switch traffic (if auto_switch enabled)
85
+ if auto_switch:
86
+ logger.info(f"πŸ”„ Switching traffic to {target_env}...")
87
+ self._switch_traffic(target_env)
88
+ result["switched"] = True
89
+ self.current_active = target_env
90
+ logger.info(f"βœ… Traffic switched to {target_env}")
91
+ else:
92
+ logger.info(f"⏸️ Auto-switch disabled. Manual switch required.")
93
+
94
+ result["success"] = True
95
+ return result
96
+
97
+ except Exception as e:
98
+ logger.error(f"❌ Blue-green deployment failed: {e}")
99
+ result["error"] = str(e)
100
+ return result
101
+
102
+ def switch_traffic(self, target_env: str) -> bool:
103
+ """Manually switch traffic to specified environment.
104
+
105
+ Args:
106
+ target_env: Target environment ('blue' or 'green')
107
+
108
+ Returns:
109
+ True if switch successful, False otherwise
110
+ """
111
+ try:
112
+ self._switch_traffic(target_env)
113
+ self.current_active = target_env
114
+ logger.info(f"βœ… Traffic switched to {target_env}")
115
+ return True
116
+ except Exception as e:
117
+ logger.error(f"❌ Failed to switch traffic: {e}")
118
+ return False
119
+
120
+ def rollback(self) -> bool:
121
+ """Rollback to previous environment.
122
+
123
+ Returns:
124
+ True if rollback successful, False otherwise
125
+ """
126
+ previous_env = "green" if self.current_active == "blue" else "blue"
127
+ logger.info(f"πŸ”„ Rolling back to {previous_env} environment...")
128
+ return self.switch_traffic(previous_env)
129
+
130
+ def _deploy_to_host(self, host: str, compose_file: str):
131
+ """Deploy application to specified host."""
132
+ # This would use the existing deploy-docker functionality
133
+ # For now, placeholder for integration
134
+ logger.info(f" Deploying {compose_file} to {host}")
135
+ # TODO: Integrate with vm_tool deploy-docker
136
+ pass
137
+
138
+ def _wait_for_health(self, host: str) -> bool:
139
+ """Wait for host to become healthy.
140
+
141
+ Args:
142
+ host: Host to check
143
+
144
+ Returns:
145
+ True if healthy within timeout, False otherwise
146
+ """
147
+ from vm_tool.health import check_http
148
+
149
+ start_time = time.time()
150
+ url = f"http://{host}{self.health_check_url}"
151
+
152
+ while time.time() - start_time < self.health_check_timeout:
153
+ try:
154
+ if check_http(url):
155
+ return True
156
+ except Exception as e:
157
+ logger.debug(f"Health check failed: {e}")
158
+
159
+ time.sleep(5)
160
+
161
+ return False
162
+
163
+ def _switch_traffic(self, target_env: str):
164
+ """Switch load balancer traffic to target environment.
165
+
166
+ Args:
167
+ target_env: Target environment ('blue' or 'green')
168
+ """
169
+ if self.load_balancer_host:
170
+ # External load balancer (e.g., AWS ALB, nginx)
171
+ logger.info(f" Updating load balancer: {self.load_balancer_host}")
172
+ # TODO: Implement load balancer update
173
+ # This would depend on the LB type (AWS, nginx, HAProxy, etc.)
174
+ else:
175
+ # Internal traffic switching (e.g., update DNS, update nginx config)
176
+ logger.info(f" Switching internal traffic to {target_env}")
177
+ # TODO: Implement internal traffic switching
178
+
179
+ # Placeholder - actual implementation would update LB/DNS
180
+ logger.info(f" Traffic switch to {target_env} complete")
181
+
182
+
183
+ class BlueGreenConfig:
184
+ """Configuration for blue-green deployments."""
185
+
186
+ def __init__(self, config_file: str = "blue-green.yml"):
187
+ self.config_file = Path(config_file)
188
+ self.config = self._load_config()
189
+
190
+ def _load_config(self) -> Dict[str, Any]:
191
+ """Load blue-green configuration."""
192
+ import yaml
193
+
194
+ if not self.config_file.exists():
195
+ return self._default_config()
196
+
197
+ with open(self.config_file) as f:
198
+ return yaml.safe_load(f)
199
+
200
+ def _default_config(self) -> Dict[str, Any]:
201
+ """Return default blue-green configuration."""
202
+ return {
203
+ "blue": {
204
+ "host": "10.0.1.10",
205
+ "port": 8000,
206
+ },
207
+ "green": {
208
+ "host": "10.0.1.11",
209
+ "port": 8000,
210
+ },
211
+ "load_balancer": {
212
+ "host": "10.0.1.1",
213
+ "type": "nginx", # or 'aws-alb', 'haproxy'
214
+ },
215
+ "health_check": {
216
+ "url": "/health",
217
+ "timeout": 300,
218
+ "interval": 5,
219
+ },
220
+ }
221
+
222
+ def save(self):
223
+ """Save configuration to file."""
224
+ import yaml
225
+
226
+ with open(self.config_file, "w") as f:
227
+ yaml.dump(self.config, f, default_flow_style=False)