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,70 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Production Deployment Script
|
|
4
|
+
# This script demonstrates a complete production deployment workflow using vm_tool
|
|
5
|
+
|
|
6
|
+
set -e # Exit on error
|
|
7
|
+
|
|
8
|
+
echo "🚀 Starting Production Deployment"
|
|
9
|
+
echo "=================================="
|
|
10
|
+
|
|
11
|
+
# Configuration
|
|
12
|
+
PROFILE="production"
|
|
13
|
+
HOST="10.0.2.10"
|
|
14
|
+
BACKUP_PATHS="/app /etc/nginx /var/www"
|
|
15
|
+
|
|
16
|
+
# Step 1: Create backup before deployment
|
|
17
|
+
echo ""
|
|
18
|
+
echo "📦 Step 1: Creating backup..."
|
|
19
|
+
vm_tool backup create \
|
|
20
|
+
--host $HOST \
|
|
21
|
+
--paths $BACKUP_PATHS
|
|
22
|
+
|
|
23
|
+
# Step 2: Check for configuration drift
|
|
24
|
+
echo ""
|
|
25
|
+
echo "🔍 Step 2: Checking for configuration drift..."
|
|
26
|
+
if ! vm_tool drift-check --host $HOST; then
|
|
27
|
+
echo "⚠️ Warning: Drift detected. Review changes before proceeding."
|
|
28
|
+
read -p "Continue anyway? (yes/no): " continue
|
|
29
|
+
if [ "$continue" != "yes" ]; then
|
|
30
|
+
echo "❌ Deployment cancelled"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Step 3: Dry-run deployment
|
|
36
|
+
echo ""
|
|
37
|
+
echo "🔍 Step 3: Running dry-run..."
|
|
38
|
+
vm_tool deploy-docker --profile $PROFILE --dry-run
|
|
39
|
+
|
|
40
|
+
# Step 4: Confirm deployment
|
|
41
|
+
echo ""
|
|
42
|
+
read -p "Proceed with deployment? (yes/no): " confirm
|
|
43
|
+
if [ "$confirm" != "yes" ]; then
|
|
44
|
+
echo "❌ Deployment cancelled"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Step 5: Deploy with health checks
|
|
49
|
+
echo ""
|
|
50
|
+
echo "🚀 Step 5: Deploying..."
|
|
51
|
+
vm_tool deploy-docker \
|
|
52
|
+
--profile $PROFILE \
|
|
53
|
+
--health-port 8000 \
|
|
54
|
+
--health-url "http://$HOST:8000/health" \
|
|
55
|
+
--health-check "docker ps | grep web"
|
|
56
|
+
|
|
57
|
+
# Step 6: Verify deployment
|
|
58
|
+
echo ""
|
|
59
|
+
echo "✅ Step 6: Verifying deployment..."
|
|
60
|
+
vm_tool history --host $HOST --limit 1
|
|
61
|
+
|
|
62
|
+
# Step 7: Final drift check
|
|
63
|
+
echo ""
|
|
64
|
+
echo "🔍 Step 7: Final drift check..."
|
|
65
|
+
vm_tool drift-check --host $HOST
|
|
66
|
+
|
|
67
|
+
echo ""
|
|
68
|
+
echo "=================================="
|
|
69
|
+
echo "✅ Production deployment complete!"
|
|
70
|
+
echo "=================================="
|
examples/rollback.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Emergency Rollback Script
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
echo "🔄 Emergency Rollback"
|
|
8
|
+
echo "===================="
|
|
9
|
+
|
|
10
|
+
# Get host from argument or prompt
|
|
11
|
+
HOST=${1:-}
|
|
12
|
+
if [ -z "$HOST" ]; then
|
|
13
|
+
read -p "Enter host IP: " HOST
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Show recent deployments
|
|
17
|
+
echo ""
|
|
18
|
+
echo "📜 Recent deployments for $HOST:"
|
|
19
|
+
vm_tool history --host $HOST --limit 5
|
|
20
|
+
|
|
21
|
+
# Get deployment ID
|
|
22
|
+
echo ""
|
|
23
|
+
read -p "Enter deployment ID to rollback to (or press Enter for previous): " DEPLOYMENT_ID
|
|
24
|
+
|
|
25
|
+
# Confirm rollback
|
|
26
|
+
echo ""
|
|
27
|
+
echo "⚠️ WARNING: This will rollback the deployment on $HOST"
|
|
28
|
+
read -p "Are you sure? (yes/no): " confirm
|
|
29
|
+
|
|
30
|
+
if [ "$confirm" != "yes" ]; then
|
|
31
|
+
echo "❌ Rollback cancelled"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Execute rollback
|
|
36
|
+
echo ""
|
|
37
|
+
echo "🔄 Rolling back..."
|
|
38
|
+
if [ -z "$DEPLOYMENT_ID" ]; then
|
|
39
|
+
vm_tool rollback --host $HOST
|
|
40
|
+
else
|
|
41
|
+
vm_tool rollback --host $HOST --to $DEPLOYMENT_ID
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Verify
|
|
45
|
+
echo ""
|
|
46
|
+
echo "✅ Rollback complete. Current deployment:"
|
|
47
|
+
vm_tool history --host $HOST --limit 1
|
|
48
|
+
|
|
49
|
+
echo ""
|
|
50
|
+
echo "🔍 Checking system health..."
|
|
51
|
+
# Add your health check commands here
|
|
52
|
+
# vm_tool drift-check --host $HOST
|
examples/setup.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Setup Script - Configure vm_tool for your environment
|
|
4
|
+
|
|
5
|
+
echo "🔧 VM Tool Setup"
|
|
6
|
+
echo "================"
|
|
7
|
+
|
|
8
|
+
# Create development profile
|
|
9
|
+
echo ""
|
|
10
|
+
echo "Creating development profile..."
|
|
11
|
+
vm_tool config create-profile dev \
|
|
12
|
+
--environment development \
|
|
13
|
+
--host 192.168.1.100 \
|
|
14
|
+
--user ubuntu \
|
|
15
|
+
--compose-file docker-compose.dev.yml
|
|
16
|
+
|
|
17
|
+
# Create staging profile
|
|
18
|
+
echo ""
|
|
19
|
+
echo "Creating staging profile..."
|
|
20
|
+
vm_tool config create-profile staging \
|
|
21
|
+
--environment staging \
|
|
22
|
+
--host 10.0.1.5 \
|
|
23
|
+
--user deploy \
|
|
24
|
+
--compose-file docker-compose.yml
|
|
25
|
+
|
|
26
|
+
# Create production profile
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Creating production profile..."
|
|
29
|
+
vm_tool config create-profile production \
|
|
30
|
+
--environment production \
|
|
31
|
+
--host 10.0.2.10 \
|
|
32
|
+
--user prod \
|
|
33
|
+
--compose-file docker-compose.yml
|
|
34
|
+
|
|
35
|
+
# Set defaults
|
|
36
|
+
echo ""
|
|
37
|
+
echo "Setting default configuration..."
|
|
38
|
+
vm_tool config set default-user ubuntu
|
|
39
|
+
vm_tool config set default-compose-file docker-compose.yml
|
|
40
|
+
|
|
41
|
+
# List all profiles
|
|
42
|
+
echo ""
|
|
43
|
+
echo "📋 Configured profiles:"
|
|
44
|
+
vm_tool config list-profiles
|
|
45
|
+
|
|
46
|
+
echo ""
|
|
47
|
+
echo "✅ Setup complete!"
|
|
48
|
+
echo ""
|
|
49
|
+
echo "Usage examples:"
|
|
50
|
+
echo " vm_tool deploy-docker --profile dev"
|
|
51
|
+
echo " vm_tool deploy-docker --profile staging --dry-run"
|
|
52
|
+
echo " vm_tool deploy-docker --profile production --health-port 8000"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Example: SSH Key Management for a VM.
|
|
3
|
+
- Generates an SSH key pair (if not present), configures the VM for SSH access, and updates local SSH config.
|
|
4
|
+
- Useful for preparing a VM for secure, passwordless SSH access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from vm_tool.ssh import SSHSetup
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
ssh_setup = SSHSetup(
|
|
12
|
+
hostname="your_vm_hostname",
|
|
13
|
+
username="your_vm_username",
|
|
14
|
+
password="your_vm_password",
|
|
15
|
+
email="your_email_for_ssh_key",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
ssh_setup.setup()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|
vm_tool/__init__.py
ADDED
|
File without changes
|
vm_tool/alerting.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Alerting system with multiple notification channels."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AlertSeverity(Enum):
|
|
12
|
+
"""Alert severity levels."""
|
|
13
|
+
|
|
14
|
+
INFO = "info"
|
|
15
|
+
WARNING = "warning"
|
|
16
|
+
ERROR = "error"
|
|
17
|
+
CRITICAL = "critical"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Alert:
|
|
22
|
+
"""Alert message."""
|
|
23
|
+
|
|
24
|
+
title: str
|
|
25
|
+
message: str
|
|
26
|
+
severity: AlertSeverity
|
|
27
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AlertChannel:
|
|
31
|
+
"""Base class for alert channels."""
|
|
32
|
+
|
|
33
|
+
def send(self, alert: Alert) -> bool:
|
|
34
|
+
"""Send alert through this channel."""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SlackAlertChannel(AlertChannel):
|
|
39
|
+
"""Send alerts to Slack."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, webhook_url: str):
|
|
42
|
+
self.webhook_url = webhook_url
|
|
43
|
+
|
|
44
|
+
def send(self, alert: Alert) -> bool:
|
|
45
|
+
"""Send alert to Slack."""
|
|
46
|
+
import requests
|
|
47
|
+
|
|
48
|
+
# Color based on severity
|
|
49
|
+
colors = {
|
|
50
|
+
AlertSeverity.INFO: "#36a64f",
|
|
51
|
+
AlertSeverity.WARNING: "#ff9900",
|
|
52
|
+
AlertSeverity.ERROR: "#ff0000",
|
|
53
|
+
AlertSeverity.CRITICAL: "#8b0000",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
payload = {
|
|
57
|
+
"attachments": [
|
|
58
|
+
{
|
|
59
|
+
"color": colors.get(alert.severity, "#808080"),
|
|
60
|
+
"title": alert.title,
|
|
61
|
+
"text": alert.message,
|
|
62
|
+
"fields": [
|
|
63
|
+
{
|
|
64
|
+
"title": "Severity",
|
|
65
|
+
"value": alert.severity.value.upper(),
|
|
66
|
+
"short": True,
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if alert.metadata:
|
|
74
|
+
for key, value in alert.metadata.items():
|
|
75
|
+
payload["attachments"][0]["fields"].append(
|
|
76
|
+
{
|
|
77
|
+
"title": key.replace("_", " ").title(),
|
|
78
|
+
"value": str(value),
|
|
79
|
+
"short": True,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
response = requests.post(self.webhook_url, json=payload)
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
logger.info("Alert sent to Slack")
|
|
87
|
+
return True
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Failed to send Slack alert: {e}")
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class PagerDutyAlertChannel(AlertChannel):
|
|
94
|
+
"""Send alerts to PagerDuty."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, integration_key: str):
|
|
97
|
+
self.integration_key = integration_key
|
|
98
|
+
|
|
99
|
+
def send(self, alert: Alert) -> bool:
|
|
100
|
+
"""Send alert to PagerDuty."""
|
|
101
|
+
import requests
|
|
102
|
+
|
|
103
|
+
# Map severity to PagerDuty severity
|
|
104
|
+
severity_map = {
|
|
105
|
+
AlertSeverity.INFO: "info",
|
|
106
|
+
AlertSeverity.WARNING: "warning",
|
|
107
|
+
AlertSeverity.ERROR: "error",
|
|
108
|
+
AlertSeverity.CRITICAL: "critical",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
payload = {
|
|
112
|
+
"routing_key": self.integration_key,
|
|
113
|
+
"event_action": "trigger",
|
|
114
|
+
"payload": {
|
|
115
|
+
"summary": alert.title,
|
|
116
|
+
"severity": severity_map.get(alert.severity, "error"),
|
|
117
|
+
"source": "vm-tool",
|
|
118
|
+
"custom_details": alert.metadata or {},
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
response = requests.post(
|
|
124
|
+
"https://events.pagerduty.com/v2/enqueue", json=payload
|
|
125
|
+
)
|
|
126
|
+
response.raise_for_status()
|
|
127
|
+
logger.info("Alert sent to PagerDuty")
|
|
128
|
+
return True
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Failed to send PagerDuty alert: {e}")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SMSAlertChannel(AlertChannel):
|
|
135
|
+
"""Send alerts via SMS using Twilio."""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self, account_sid: str, auth_token: str, from_number: str, to_numbers: List[str]
|
|
139
|
+
):
|
|
140
|
+
self.account_sid = account_sid
|
|
141
|
+
self.auth_token = auth_token
|
|
142
|
+
self.from_number = from_number
|
|
143
|
+
self.to_numbers = to_numbers
|
|
144
|
+
|
|
145
|
+
def send(self, alert: Alert) -> bool:
|
|
146
|
+
"""Send alert via SMS."""
|
|
147
|
+
try:
|
|
148
|
+
from twilio.rest import Client
|
|
149
|
+
|
|
150
|
+
client = Client(self.account_sid, self.auth_token)
|
|
151
|
+
|
|
152
|
+
# Format message
|
|
153
|
+
message = f"[{alert.severity.value.upper()}] {alert.title}\n{alert.message}"
|
|
154
|
+
|
|
155
|
+
# Send to all numbers
|
|
156
|
+
for number in self.to_numbers:
|
|
157
|
+
client.messages.create(body=message, from_=self.from_number, to=number)
|
|
158
|
+
|
|
159
|
+
logger.info(f"Alert sent via SMS to {len(self.to_numbers)} recipients")
|
|
160
|
+
return True
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"Failed to send SMS alert: {e}")
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class EmailAlertChannel(AlertChannel):
|
|
167
|
+
"""Send alerts via email."""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
smtp_host: str,
|
|
172
|
+
smtp_port: int,
|
|
173
|
+
from_email: str,
|
|
174
|
+
to_emails: List[str],
|
|
175
|
+
smtp_user: Optional[str] = None,
|
|
176
|
+
smtp_password: Optional[str] = None,
|
|
177
|
+
):
|
|
178
|
+
self.smtp_host = smtp_host
|
|
179
|
+
self.smtp_port = smtp_port
|
|
180
|
+
self.from_email = from_email
|
|
181
|
+
self.to_emails = to_emails
|
|
182
|
+
self.smtp_user = smtp_user
|
|
183
|
+
self.smtp_password = smtp_password
|
|
184
|
+
|
|
185
|
+
def send(self, alert: Alert) -> bool:
|
|
186
|
+
"""Send alert via email."""
|
|
187
|
+
from vm_tool.notifications import EmailNotifier
|
|
188
|
+
|
|
189
|
+
notifier = EmailNotifier(
|
|
190
|
+
smtp_host=self.smtp_host,
|
|
191
|
+
smtp_port=self.smtp_port,
|
|
192
|
+
smtp_user=self.smtp_user,
|
|
193
|
+
smtp_password=self.smtp_password,
|
|
194
|
+
from_email=self.from_email,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
subject = f"[{alert.severity.value.upper()}] {alert.title}"
|
|
198
|
+
|
|
199
|
+
return notifier.send_email(
|
|
200
|
+
to_emails=self.to_emails, subject=subject, body=alert.message
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class AlertingSystem:
|
|
205
|
+
"""Centralized alerting system."""
|
|
206
|
+
|
|
207
|
+
def __init__(self):
|
|
208
|
+
self.channels: List[AlertChannel] = []
|
|
209
|
+
|
|
210
|
+
def add_channel(self, channel: AlertChannel):
|
|
211
|
+
"""Add an alert channel."""
|
|
212
|
+
self.channels.append(channel)
|
|
213
|
+
|
|
214
|
+
def send_alert(self, alert: Alert):
|
|
215
|
+
"""Send alert through all channels."""
|
|
216
|
+
logger.info(f"Sending alert: {alert.title} ({alert.severity.value})")
|
|
217
|
+
|
|
218
|
+
for channel in self.channels:
|
|
219
|
+
try:
|
|
220
|
+
channel.send(alert)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Failed to send alert through channel: {e}")
|
|
223
|
+
|
|
224
|
+
def deployment_failed(self, host: str, error: str, **metadata):
|
|
225
|
+
"""Send deployment failure alert."""
|
|
226
|
+
alert = Alert(
|
|
227
|
+
title="Deployment Failed",
|
|
228
|
+
message=f"Deployment to {host} failed: {error}",
|
|
229
|
+
severity=AlertSeverity.ERROR,
|
|
230
|
+
metadata={"host": host, "error": error, **metadata},
|
|
231
|
+
)
|
|
232
|
+
self.send_alert(alert)
|
|
233
|
+
|
|
234
|
+
def deployment_succeeded(self, host: str, duration: float, **metadata):
|
|
235
|
+
"""Send deployment success notification."""
|
|
236
|
+
alert = Alert(
|
|
237
|
+
title="Deployment Successful",
|
|
238
|
+
message=f"Deployment to {host} completed in {duration:.2f}s",
|
|
239
|
+
severity=AlertSeverity.INFO,
|
|
240
|
+
metadata={"host": host, "duration": duration, **metadata},
|
|
241
|
+
)
|
|
242
|
+
self.send_alert(alert)
|
|
243
|
+
|
|
244
|
+
def health_check_failed(self, host: str, check_type: str, **metadata):
|
|
245
|
+
"""Send health check failure alert."""
|
|
246
|
+
alert = Alert(
|
|
247
|
+
title="Health Check Failed",
|
|
248
|
+
message=f"Health check '{check_type}' failed on {host}",
|
|
249
|
+
severity=AlertSeverity.WARNING,
|
|
250
|
+
metadata={"host": host, "check_type": check_type, **metadata},
|
|
251
|
+
)
|
|
252
|
+
self.send_alert(alert)
|
|
253
|
+
|
|
254
|
+
def critical_error(self, title: str, message: str, **metadata):
|
|
255
|
+
"""Send critical error alert."""
|
|
256
|
+
alert = Alert(
|
|
257
|
+
title=title,
|
|
258
|
+
message=message,
|
|
259
|
+
severity=AlertSeverity.CRITICAL,
|
|
260
|
+
metadata=metadata,
|
|
261
|
+
)
|
|
262
|
+
self.send_alert(alert)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Global alerting system instance
|
|
266
|
+
_alerting_system = None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_alerting_system() -> AlertingSystem:
|
|
270
|
+
"""Get global alerting system instance."""
|
|
271
|
+
global _alerting_system
|
|
272
|
+
if _alerting_system is None:
|
|
273
|
+
_alerting_system = AlertingSystem()
|
|
274
|
+
return _alerting_system
|
vm_tool/audit.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Audit logging for all deployment operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import json
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuditEventType(Enum):
|
|
14
|
+
"""Types of audit events."""
|
|
15
|
+
|
|
16
|
+
DEPLOYMENT_STARTED = "deployment.started"
|
|
17
|
+
DEPLOYMENT_SUCCESS = "deployment.success"
|
|
18
|
+
DEPLOYMENT_FAILED = "deployment.failed"
|
|
19
|
+
ROLLBACK_STARTED = "rollback.started"
|
|
20
|
+
ROLLBACK_SUCCESS = "rollback.success"
|
|
21
|
+
CONFIG_CHANGED = "config.changed"
|
|
22
|
+
SECRET_ACCESSED = "secret.accessed"
|
|
23
|
+
USER_LOGIN = "user.login"
|
|
24
|
+
PERMISSION_DENIED = "permission.denied"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuditLogger:
|
|
28
|
+
"""Centralized audit logging system."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, audit_dir: str = ".vm_tool/audit"):
|
|
31
|
+
self.audit_dir = Path(audit_dir)
|
|
32
|
+
self.audit_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
self.audit_file = self.audit_dir / "audit.jsonl"
|
|
34
|
+
|
|
35
|
+
def log_event(
|
|
36
|
+
self,
|
|
37
|
+
event_type: AuditEventType,
|
|
38
|
+
user: str,
|
|
39
|
+
action: str,
|
|
40
|
+
resource: str,
|
|
41
|
+
success: bool = True,
|
|
42
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
43
|
+
):
|
|
44
|
+
"""Log an audit event."""
|
|
45
|
+
event = {
|
|
46
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
47
|
+
"event_type": event_type.value,
|
|
48
|
+
"user": user,
|
|
49
|
+
"action": action,
|
|
50
|
+
"resource": resource,
|
|
51
|
+
"success": success,
|
|
52
|
+
"metadata": metadata or {},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Append to audit log
|
|
56
|
+
with open(self.audit_file, "a") as f:
|
|
57
|
+
f.write(json.dumps(event) + "\n")
|
|
58
|
+
|
|
59
|
+
logger.info(
|
|
60
|
+
f"Audit: {user} {action} {resource} - {'success' if success else 'failed'}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def get_audit_trail(self, limit: int = 100) -> list:
|
|
64
|
+
"""Get recent audit events."""
|
|
65
|
+
if not self.audit_file.exists():
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
events = []
|
|
69
|
+
with open(self.audit_file) as f:
|
|
70
|
+
for line in f:
|
|
71
|
+
events.append(json.loads(line))
|
|
72
|
+
|
|
73
|
+
return events[-limit:]
|
|
74
|
+
|
|
75
|
+
def search_events(
|
|
76
|
+
self,
|
|
77
|
+
user: Optional[str] = None,
|
|
78
|
+
event_type: Optional[str] = None,
|
|
79
|
+
start_time: Optional[datetime] = None,
|
|
80
|
+
end_time: Optional[datetime] = None,
|
|
81
|
+
) -> list:
|
|
82
|
+
"""Search audit events with filters."""
|
|
83
|
+
events = self.get_audit_trail(limit=10000)
|
|
84
|
+
|
|
85
|
+
filtered = []
|
|
86
|
+
for event in events:
|
|
87
|
+
if user and event.get("user") != user:
|
|
88
|
+
continue
|
|
89
|
+
if event_type and event.get("event_type") != event_type:
|
|
90
|
+
continue
|
|
91
|
+
if start_time:
|
|
92
|
+
event_time = datetime.fromisoformat(
|
|
93
|
+
event["timestamp"].replace("Z", "+00:00")
|
|
94
|
+
)
|
|
95
|
+
if event_time < start_time:
|
|
96
|
+
continue
|
|
97
|
+
if end_time:
|
|
98
|
+
event_time = datetime.fromisoformat(
|
|
99
|
+
event["timestamp"].replace("Z", "+00:00")
|
|
100
|
+
)
|
|
101
|
+
if event_time > end_time:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
filtered.append(event)
|
|
105
|
+
|
|
106
|
+
return filtered
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Global audit logger
|
|
110
|
+
_audit_logger = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_audit_logger() -> AuditLogger:
|
|
114
|
+
"""Get global audit logger instance."""
|
|
115
|
+
global _audit_logger
|
|
116
|
+
if _audit_logger is None:
|
|
117
|
+
_audit_logger = AuditLogger()
|
|
118
|
+
return _audit_logger
|