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
vm_tool/history.py ADDED
@@ -0,0 +1,131 @@
1
+ """Deployment history tracking and rollback functionality."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class DeploymentHistory:
13
+ """Manages deployment history for rollback capability."""
14
+
15
+ def __init__(self, history_dir: Optional[Path] = None):
16
+ if history_dir is None:
17
+ history_dir = Path.home() / ".vm_tool"
18
+ self.history_dir = history_dir
19
+ self.history_file = self.history_dir / "deployment_history.json"
20
+ self._ensure_history_dir()
21
+
22
+ def _ensure_history_dir(self):
23
+ """Create history directory if it doesn't exist."""
24
+ self.history_dir.mkdir(parents=True, exist_ok=True)
25
+
26
+ def _load_history(self) -> List[Dict[str, Any]]:
27
+ """Load deployment history from file."""
28
+ if not self.history_file.exists():
29
+ return []
30
+ try:
31
+ with open(self.history_file, "r") as f:
32
+ return json.load(f)
33
+ except json.JSONDecodeError:
34
+ logger.warning("Invalid history file, returning empty history")
35
+ return []
36
+
37
+ def _save_history(self, history: List[Dict[str, Any]]):
38
+ """Save deployment history to file."""
39
+ with open(self.history_file, "w") as f:
40
+ json.dump(history, f, indent=2)
41
+
42
+ def record_deployment(
43
+ self,
44
+ host: str,
45
+ compose_file: str,
46
+ compose_hash: str,
47
+ git_commit: Optional[str] = None,
48
+ service_name: str = "default",
49
+ status: str = "success",
50
+ error: Optional[str] = None,
51
+ ) -> str:
52
+ """Record a deployment in history and return deployment ID."""
53
+ history = self._load_history()
54
+
55
+ deployment_id = datetime.now().strftime("%Y%m%d_%H%M%S")
56
+
57
+ deployment_record = {
58
+ "id": deployment_id,
59
+ "timestamp": datetime.now().isoformat(),
60
+ "host": host,
61
+ "service_name": service_name,
62
+ "compose_file": compose_file,
63
+ "compose_hash": compose_hash,
64
+ "git_commit": git_commit,
65
+ "status": status,
66
+ "error": error,
67
+ }
68
+
69
+ history.append(deployment_record)
70
+
71
+ # Keep only last 100 deployments
72
+ if len(history) > 100:
73
+ history = history[-100:]
74
+
75
+ self._save_history(history)
76
+ logger.info(f"Recorded deployment: {deployment_id}")
77
+
78
+ return deployment_id
79
+
80
+ def get_history(
81
+ self, host: Optional[str] = None, limit: int = 10
82
+ ) -> List[Dict[str, Any]]:
83
+ """Get deployment history, optionally filtered by host."""
84
+ history = self._load_history()
85
+
86
+ if host:
87
+ history = [d for d in history if d.get("host") == host]
88
+
89
+ # Return most recent first
90
+ history.reverse()
91
+
92
+ return history[:limit]
93
+
94
+ def get_deployment(self, deployment_id: str) -> Optional[Dict[str, Any]]:
95
+ """Get a specific deployment by ID."""
96
+ history = self._load_history()
97
+ for deployment in history:
98
+ if deployment.get("id") == deployment_id:
99
+ return deployment
100
+ return None
101
+
102
+ def get_previous_deployment(
103
+ self, host: str, service_name: str = "default"
104
+ ) -> Optional[Dict[str, Any]]:
105
+ """Get the previous successful deployment for a host/service."""
106
+ history = self._load_history()
107
+
108
+ # Filter by host and service, get successful deployments
109
+ matching = [
110
+ d
111
+ for d in history
112
+ if d.get("host") == host
113
+ and d.get("service_name") == service_name
114
+ and d.get("status") == "success"
115
+ ]
116
+
117
+ if len(matching) >= 2:
118
+ # Return second-to-last (previous deployment)
119
+ return matching[-2]
120
+
121
+ return None
122
+
123
+ def get_rollback_info(
124
+ self, host: str, deployment_id: Optional[str] = None
125
+ ) -> Optional[Dict[str, Any]]:
126
+ """Get rollback information for a deployment."""
127
+ if deployment_id:
128
+ return self.get_deployment(deployment_id)
129
+ else:
130
+ # Get previous deployment
131
+ return self.get_previous_deployment(host)
vm_tool/kubernetes.py ADDED
@@ -0,0 +1,89 @@
1
+ """Kubernetes native support framework."""
2
+
3
+ import logging
4
+ from typing import Dict, Any, Optional
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class KubernetesDeployment:
10
+ """Kubernetes deployment manager (requires kubectl/kubernetes python client)."""
11
+
12
+ def __init__(self, kubeconfig: Optional[str] = None, namespace: str = "default"):
13
+ self.kubeconfig = kubeconfig
14
+ self.namespace = namespace
15
+ logger.info(f"Kubernetes deployment initialized (namespace: {namespace})")
16
+ # TODO: Initialize kubernetes client
17
+
18
+ def deploy_helm_chart(
19
+ self, chart_name: str, release_name: str, values: Dict[str, Any]
20
+ ) -> bool:
21
+ """Deploy Helm chart."""
22
+ logger.info(f"Deploying Helm chart: {chart_name} as {release_name}")
23
+ # TODO: Implement with helm or kubernetes client
24
+ return True
25
+
26
+ def deploy_manifest(self, manifest_file: str) -> bool:
27
+ """Deploy Kubernetes manifest."""
28
+ logger.info(f"Deploying manifest: {manifest_file}")
29
+ # TODO: Implement with kubectl or kubernetes client
30
+ return True
31
+
32
+ def get_pod_status(self, pod_name: str) -> str:
33
+ """Get pod status."""
34
+ # TODO: Implement
35
+ return "Running"
36
+
37
+ def scale_deployment(self, deployment_name: str, replicas: int) -> bool:
38
+ """Scale deployment."""
39
+ logger.info(f"Scaling {deployment_name} to {replicas} replicas")
40
+ # TODO: Implement
41
+ return True
42
+
43
+ def rollback_deployment(
44
+ self, deployment_name: str, revision: Optional[int] = None
45
+ ) -> bool:
46
+ """Rollback deployment."""
47
+ logger.info(f"Rolling back deployment: {deployment_name}")
48
+ # TODO: Implement
49
+ return True
50
+
51
+
52
+ class ServiceMeshIntegration:
53
+ """Service mesh integration (Istio/Linkerd)."""
54
+
55
+ def __init__(self, mesh_type: str = "istio"):
56
+ self.mesh_type = mesh_type
57
+ logger.info(f"Service mesh integration: {mesh_type}")
58
+
59
+ def configure_traffic_split(self, service: str, versions: Dict[str, int]) -> bool:
60
+ """Configure traffic splitting between versions."""
61
+ logger.info(f"Configuring traffic split for {service}: {versions}")
62
+ # TODO: Implement Istio VirtualService or Linkerd TrafficSplit
63
+ return True
64
+
65
+ def enable_mtls(self, namespace: str) -> bool:
66
+ """Enable mutual TLS."""
67
+ logger.info(f"Enabling mTLS for namespace: {namespace}")
68
+ # TODO: Implement
69
+ return True
70
+
71
+
72
+ class GitOpsIntegration:
73
+ """GitOps integration (ArgoCD/Flux)."""
74
+
75
+ def __init__(self, gitops_tool: str = "argocd"):
76
+ self.gitops_tool = gitops_tool
77
+ logger.info(f"GitOps integration: {gitops_tool}")
78
+
79
+ def create_application(self, name: str, repo_url: str, path: str) -> bool:
80
+ """Create GitOps application."""
81
+ logger.info(f"Creating GitOps app: {name} from {repo_url}/{path}")
82
+ # TODO: Implement ArgoCD or Flux application creation
83
+ return True
84
+
85
+ def sync_application(self, name: str) -> bool:
86
+ """Sync GitOps application."""
87
+ logger.info(f"Syncing GitOps app: {name}")
88
+ # TODO: Implement
89
+ return True
vm_tool/metrics.py ADDED
@@ -0,0 +1,183 @@
1
+ """Metrics collection and export for deployments."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Dict, Any, Optional
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass
14
+ class DeploymentMetrics:
15
+ """Metrics for a deployment."""
16
+
17
+ deployment_id: str
18
+ host: str
19
+ start_time: float = field(default_factory=time.time)
20
+ end_time: Optional[float] = None
21
+ duration: Optional[float] = None
22
+ success: bool = False
23
+ error: Optional[str] = None
24
+ metadata: Dict[str, Any] = field(default_factory=dict)
25
+
26
+ def finish(self, success: bool = True, error: Optional[str] = None):
27
+ """Mark deployment as finished."""
28
+ self.end_time = time.time()
29
+ self.duration = self.end_time - self.start_time
30
+ self.success = success
31
+ self.error = error
32
+
33
+ def to_dict(self) -> Dict[str, Any]:
34
+ """Convert to dictionary."""
35
+ return {
36
+ "deployment_id": self.deployment_id,
37
+ "host": self.host,
38
+ "start_time": datetime.fromtimestamp(self.start_time).isoformat(),
39
+ "end_time": (
40
+ datetime.fromtimestamp(self.end_time).isoformat()
41
+ if self.end_time
42
+ else None
43
+ ),
44
+ "duration_seconds": self.duration,
45
+ "success": self.success,
46
+ "error": self.error,
47
+ **self.metadata,
48
+ }
49
+
50
+
51
+ class MetricsCollector:
52
+ """Collect and export deployment metrics."""
53
+
54
+ def __init__(self, export_dir: str = ".vm_tool/metrics"):
55
+ self.export_dir = Path(export_dir)
56
+ self.export_dir.mkdir(parents=True, exist_ok=True)
57
+ self.current_deployment: Optional[DeploymentMetrics] = None
58
+
59
+ def start_deployment(
60
+ self, deployment_id: str, host: str, **metadata
61
+ ) -> DeploymentMetrics:
62
+ """Start tracking a deployment."""
63
+ self.current_deployment = DeploymentMetrics(
64
+ deployment_id=deployment_id, host=host, metadata=metadata
65
+ )
66
+ logger.info(f"📊 Started tracking deployment: {deployment_id}")
67
+ return self.current_deployment
68
+
69
+ def finish_deployment(self, success: bool = True, error: Optional[str] = None):
70
+ """Finish tracking current deployment."""
71
+ if self.current_deployment:
72
+ self.current_deployment.finish(success, error)
73
+ self._export_metrics(self.current_deployment)
74
+ logger.info(
75
+ f"📊 Deployment metrics recorded: {self.current_deployment.duration:.2f}s"
76
+ )
77
+
78
+ def _export_metrics(self, metrics: DeploymentMetrics):
79
+ """Export metrics to file."""
80
+ import json
81
+
82
+ # Export to JSON
83
+ metrics_file = self.export_dir / f"{metrics.deployment_id}.json"
84
+ with open(metrics_file, "w") as f:
85
+ json.dump(metrics.to_dict(), f, indent=2)
86
+
87
+ # Append to metrics log
88
+ log_file = self.export_dir / "deployments.jsonl"
89
+ with open(log_file, "a") as f:
90
+ f.write(json.dumps(metrics.to_dict()) + "\n")
91
+
92
+ def export_prometheus(
93
+ self, metrics: DeploymentMetrics, output_file: str = "metrics.prom"
94
+ ):
95
+ """Export metrics in Prometheus format."""
96
+ prom_file = self.export_dir / output_file
97
+
98
+ with open(prom_file, "w") as f:
99
+ # Deployment duration
100
+ f.write(f"# HELP deployment_duration_seconds Time taken for deployment\n")
101
+ f.write(f"# TYPE deployment_duration_seconds gauge\n")
102
+ f.write(
103
+ f'deployment_duration_seconds{{host="{metrics.host}",deployment_id="{metrics.deployment_id}"}} {metrics.duration or 0}\n\n'
104
+ )
105
+
106
+ # Deployment success
107
+ f.write(
108
+ f"# HELP deployment_success Whether deployment succeeded (1) or failed (0)\n"
109
+ )
110
+ f.write(f"# TYPE deployment_success gauge\n")
111
+ f.write(
112
+ f'deployment_success{{host="{metrics.host}",deployment_id="{metrics.deployment_id}"}} {1 if metrics.success else 0}\n\n'
113
+ )
114
+
115
+ # Deployment timestamp
116
+ f.write(f"# HELP deployment_timestamp_seconds Deployment start time\n")
117
+ f.write(f"# TYPE deployment_timestamp_seconds gauge\n")
118
+ f.write(
119
+ f'deployment_timestamp_seconds{{host="{metrics.host}",deployment_id="{metrics.deployment_id}"}} {metrics.start_time}\n'
120
+ )
121
+
122
+ logger.info(f"📊 Prometheus metrics exported to {prom_file}")
123
+
124
+ def get_stats(self, limit: int = 100) -> Dict[str, Any]:
125
+ """Get deployment statistics."""
126
+ import json
127
+
128
+ log_file = self.export_dir / "deployments.jsonl"
129
+ if not log_file.exists():
130
+ return {"total": 0, "success": 0, "failed": 0}
131
+
132
+ deployments = []
133
+ with open(log_file) as f:
134
+ for line in f:
135
+ deployments.append(json.loads(line))
136
+
137
+ # Get recent deployments
138
+ recent = deployments[-limit:]
139
+
140
+ total = len(recent)
141
+ success = sum(1 for d in recent if d.get("success"))
142
+ failed = total - success
143
+
144
+ durations = [d["duration_seconds"] for d in recent if d.get("duration_seconds")]
145
+ avg_duration = sum(durations) / len(durations) if durations else 0
146
+
147
+ return {
148
+ "total_deployments": total,
149
+ "successful": success,
150
+ "failed": failed,
151
+ "success_rate": (success / total * 100) if total > 0 else 0,
152
+ "average_duration_seconds": avg_duration,
153
+ "recent_deployments": recent[-10:], # Last 10
154
+ }
155
+
156
+ def print_stats(self):
157
+ """Print deployment statistics."""
158
+ stats = self.get_stats()
159
+
160
+ print("\n📊 Deployment Statistics")
161
+ print("=" * 50)
162
+ print(f"Total Deployments: {stats['total_deployments']}")
163
+ print(f"Successful: {stats['successful']} ({stats['success_rate']:.1f}%)")
164
+ print(f"Failed: {stats['failed']}")
165
+ print(f"Average Duration: {stats['average_duration_seconds']:.2f}s")
166
+ print("\nRecent Deployments:")
167
+ for d in stats["recent_deployments"]:
168
+ status = "✅" if d["success"] else "❌"
169
+ print(
170
+ f" {status} {d['deployment_id']} - {d['host']} - {d['duration_seconds']:.2f}s"
171
+ )
172
+
173
+
174
+ # Global metrics collector instance
175
+ _metrics_collector = None
176
+
177
+
178
+ def get_metrics_collector() -> MetricsCollector:
179
+ """Get global metrics collector instance."""
180
+ global _metrics_collector
181
+ if _metrics_collector is None:
182
+ _metrics_collector = MetricsCollector()
183
+ return _metrics_collector
@@ -0,0 +1,152 @@
1
+ """Email notification support for deployment events."""
2
+
3
+ import logging
4
+ import smtplib
5
+ from email.mime.text import MIMEText
6
+ from email.mime.multipart import MIMEMultipart
7
+ from typing import List, Optional
8
+ from datetime import datetime
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class EmailNotifier:
14
+ """Send email notifications for deployment events."""
15
+
16
+ def __init__(
17
+ self,
18
+ smtp_host: str = "localhost",
19
+ smtp_port: int = 587,
20
+ smtp_user: Optional[str] = None,
21
+ smtp_password: Optional[str] = None,
22
+ from_email: str = "vm-tool@localhost",
23
+ use_tls: bool = True,
24
+ ):
25
+ self.smtp_host = smtp_host
26
+ self.smtp_port = smtp_port
27
+ self.smtp_user = smtp_user
28
+ self.smtp_password = smtp_password
29
+ self.from_email = from_email
30
+ self.use_tls = use_tls
31
+
32
+ def send_email(
33
+ self, to_emails: List[str], subject: str, body: str, html: bool = False
34
+ ) -> bool:
35
+ """Send email notification.
36
+
37
+ Args:
38
+ to_emails: List of recipient email addresses
39
+ subject: Email subject
40
+ body: Email body
41
+ html: If True, send as HTML email
42
+
43
+ Returns:
44
+ True if email sent successfully, False otherwise
45
+ """
46
+ try:
47
+ msg = MIMEMultipart("alternative")
48
+ msg["Subject"] = subject
49
+ msg["From"] = self.from_email
50
+ msg["To"] = ", ".join(to_emails)
51
+ msg["Date"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
52
+
53
+ if html:
54
+ msg.attach(MIMEText(body, "html"))
55
+ else:
56
+ msg.attach(MIMEText(body, "plain"))
57
+
58
+ # Connect to SMTP server
59
+ if self.use_tls:
60
+ server = smtplib.SMTP(self.smtp_host, self.smtp_port)
61
+ server.starttls()
62
+ else:
63
+ server = smtplib.SMTP(self.smtp_host, self.smtp_port)
64
+
65
+ # Login if credentials provided
66
+ if self.smtp_user and self.smtp_password:
67
+ server.login(self.smtp_user, self.smtp_password)
68
+
69
+ # Send email
70
+ server.sendmail(self.from_email, to_emails, msg.as_string())
71
+ server.quit()
72
+
73
+ logger.info(f"Email sent successfully to {', '.join(to_emails)}")
74
+ return True
75
+
76
+ except Exception as e:
77
+ logger.error(f"Failed to send email: {e}")
78
+ return False
79
+
80
+ def deployment_success(
81
+ self, to_emails: List[str], host: str, duration: float, **kwargs
82
+ ):
83
+ """Send deployment success notification."""
84
+ subject = f"✅ Deployment Successful - {host}"
85
+ body = f"""
86
+ Deployment completed successfully!
87
+
88
+ Host: {host}
89
+ Duration: {duration:.2f} seconds
90
+ Time: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}
91
+
92
+ Additional Details:
93
+ {self._format_kwargs(kwargs)}
94
+ """
95
+ return self.send_email(to_emails, subject, body)
96
+
97
+ def deployment_failed(self, to_emails: List[str], host: str, error: str, **kwargs):
98
+ """Send deployment failure notification."""
99
+ subject = f"❌ Deployment Failed - {host}"
100
+ body = f"""
101
+ Deployment failed!
102
+
103
+ Host: {host}
104
+ Error: {error}
105
+ Time: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}
106
+
107
+ Additional Details:
108
+ {self._format_kwargs(kwargs)}
109
+
110
+ Please check the logs for more information.
111
+ """
112
+ return self.send_email(to_emails, subject, body)
113
+
114
+ def rollback_success(self, to_emails: List[str], host: str, **kwargs):
115
+ """Send rollback success notification."""
116
+ subject = f"🔄 Rollback Successful - {host}"
117
+ body = f"""
118
+ Rollback completed successfully!
119
+
120
+ Host: {host}
121
+ Time: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}
122
+
123
+ Additional Details:
124
+ {self._format_kwargs(kwargs)}
125
+ """
126
+ return self.send_email(to_emails, subject, body)
127
+
128
+ def health_check_failed(
129
+ self, to_emails: List[str], host: str, check_type: str, **kwargs
130
+ ):
131
+ """Send health check failure notification."""
132
+ subject = f"⚠️ Health Check Failed - {host}"
133
+ body = f"""
134
+ Health check failed!
135
+
136
+ Host: {host}
137
+ Check Type: {check_type}
138
+ Time: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}
139
+
140
+ Additional Details:
141
+ {self._format_kwargs(kwargs)}
142
+
143
+ Please investigate immediately.
144
+ """
145
+ return self.send_email(to_emails, subject, body)
146
+
147
+ @staticmethod
148
+ def _format_kwargs(kwargs: dict) -> str:
149
+ """Format kwargs as readable string."""
150
+ if not kwargs:
151
+ return "None"
152
+ return "\n".join(f" {k}: {v}" for k, v in kwargs.items())
vm_tool/plugins.py ADDED
@@ -0,0 +1,119 @@
1
+ """Plugin system for extensibility."""
2
+
3
+ import logging
4
+ from typing import Dict, Any, Callable, List
5
+ from pathlib import Path
6
+ import importlib.util
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class Plugin:
12
+ """Base plugin class."""
13
+
14
+ def __init__(self, name: str, version: str):
15
+ self.name = name
16
+ self.version = version
17
+
18
+ def on_deployment_start(self, context: Dict[str, Any]):
19
+ """Called before deployment starts."""
20
+ pass
21
+
22
+ def on_deployment_success(self, context: Dict[str, Any]):
23
+ """Called after successful deployment."""
24
+ pass
25
+
26
+ def on_deployment_failure(self, context: Dict[str, Any]):
27
+ """Called after failed deployment."""
28
+ pass
29
+
30
+
31
+ class PluginManager:
32
+ """Manage plugins."""
33
+
34
+ def __init__(self, plugin_dir: str = ".vm_tool/plugins"):
35
+ self.plugin_dir = Path(plugin_dir)
36
+ self.plugin_dir.mkdir(parents=True, exist_ok=True)
37
+ self.plugins: List[Plugin] = []
38
+
39
+ def load_plugin(self, plugin_path: str):
40
+ """Load a plugin from file."""
41
+ logger.info(f"Loading plugin: {plugin_path}")
42
+
43
+ # TODO: Implement plugin loading
44
+ # - Load Python module
45
+ # - Instantiate plugin class
46
+ # - Register hooks
47
+
48
+ def register_plugin(self, plugin: Plugin):
49
+ """Register a plugin."""
50
+ self.plugins.append(plugin)
51
+ logger.info(f"Registered plugin: {plugin.name} v{plugin.version}")
52
+
53
+ def trigger_hook(self, hook_name: str, context: Dict[str, Any]):
54
+ """Trigger plugin hooks."""
55
+ for plugin in self.plugins:
56
+ try:
57
+ hook = getattr(plugin, hook_name, None)
58
+ if hook and callable(hook):
59
+ hook(context)
60
+ except Exception as e:
61
+ logger.error(f"Plugin {plugin.name} hook {hook_name} failed: {e}")
62
+
63
+
64
+ # Integration stubs for nice-to-have features
65
+ class SlackBot:
66
+ """Slack bot integration."""
67
+
68
+ def __init__(self, bot_token: str):
69
+ self.bot_token = bot_token
70
+ logger.info("Slack bot initialized")
71
+
72
+ def send_message(self, channel: str, message: str):
73
+ """Send message to Slack channel."""
74
+ logger.info(f"Sending to #{channel}: {message}")
75
+ # TODO: Implement with slack_sdk
76
+
77
+
78
+ class DiscordIntegration:
79
+ """Discord integration."""
80
+
81
+ def __init__(self, webhook_url: str):
82
+ self.webhook_url = webhook_url
83
+ logger.info("Discord integration initialized")
84
+
85
+ def send_notification(self, message: str):
86
+ """Send notification to Discord."""
87
+ logger.info(f"Discord notification: {message}")
88
+ # TODO: Implement with discord.py
89
+
90
+
91
+ class TerraformProvider:
92
+ """Terraform provider stub."""
93
+
94
+ def __init__(self):
95
+ logger.info("Terraform provider initialized")
96
+ # TODO: Implement Terraform provider
97
+ # This would be a separate Go project
98
+
99
+
100
+ class GitHubApp:
101
+ """GitHub App integration."""
102
+
103
+ def __init__(self, app_id: str, private_key: str):
104
+ self.app_id = app_id
105
+ logger.info(f"GitHub App initialized: {app_id}")
106
+ # TODO: Implement with PyGithub
107
+
108
+ def create_deployment(self, repo: str, ref: str):
109
+ """Create GitHub deployment."""
110
+ logger.info(f"Creating GitHub deployment: {repo}@{ref}")
111
+ # TODO: Implement
112
+
113
+
114
+ class VSCodeExtension:
115
+ """VS Code extension stub."""
116
+
117
+ # This would be a separate TypeScript/JavaScript project
118
+ # Placeholder for documentation
119
+ pass