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/cloud.py ADDED
@@ -0,0 +1,125 @@
1
+ """Multi-cloud support framework (AWS, GCP, Azure)."""
2
+
3
+ import logging
4
+ from typing import Optional, Dict, Any
5
+ from abc import ABC, abstractmethod
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class CloudProvider(ABC):
11
+ """Base class for cloud providers."""
12
+
13
+ @abstractmethod
14
+ def deploy_vm(self, config: Dict[str, Any]) -> str:
15
+ """Deploy VM and return instance ID."""
16
+ pass
17
+
18
+ @abstractmethod
19
+ def get_vm_status(self, instance_id: str) -> str:
20
+ """Get VM status."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def terminate_vm(self, instance_id: str) -> bool:
25
+ """Terminate VM."""
26
+ pass
27
+
28
+
29
+ class AWSProvider(CloudProvider):
30
+ """AWS cloud provider (requires boto3)."""
31
+
32
+ def __init__(self, region: str = "us-east-1"):
33
+ self.region = region
34
+ logger.info(f"AWS provider initialized (region: {region})")
35
+ # TODO: Initialize boto3 client
36
+
37
+ def deploy_vm(self, config: Dict[str, Any]) -> str:
38
+ """Deploy EC2 instance."""
39
+ logger.info("Deploying AWS EC2 instance...")
40
+ # TODO: Implement with boto3
41
+ return "i-placeholder"
42
+
43
+ def get_vm_status(self, instance_id: str) -> str:
44
+ """Get EC2 instance status."""
45
+ # TODO: Implement with boto3
46
+ return "running"
47
+
48
+ def terminate_vm(self, instance_id: str) -> bool:
49
+ """Terminate EC2 instance."""
50
+ logger.info(f"Terminating EC2 instance: {instance_id}")
51
+ # TODO: Implement with boto3
52
+ return True
53
+
54
+
55
+ class GCPProvider(CloudProvider):
56
+ """GCP cloud provider (requires google-cloud-compute)."""
57
+
58
+ def __init__(self, project_id: str, zone: str = "us-central1-a"):
59
+ self.project_id = project_id
60
+ self.zone = zone
61
+ logger.info(f"GCP provider initialized (project: {project_id}, zone: {zone})")
62
+ # TODO: Initialize GCP client
63
+
64
+ def deploy_vm(self, config: Dict[str, Any]) -> str:
65
+ """Deploy GCE instance."""
66
+ logger.info("Deploying GCP Compute Engine instance...")
67
+ # TODO: Implement with google-cloud-compute
68
+ return "gcp-instance-placeholder"
69
+
70
+ def get_vm_status(self, instance_id: str) -> str:
71
+ """Get GCE instance status."""
72
+ # TODO: Implement
73
+ return "RUNNING"
74
+
75
+ def terminate_vm(self, instance_id: str) -> bool:
76
+ """Terminate GCE instance."""
77
+ logger.info(f"Terminating GCE instance: {instance_id}")
78
+ # TODO: Implement
79
+ return True
80
+
81
+
82
+ class AzureProvider(CloudProvider):
83
+ """Azure cloud provider (requires azure-mgmt-compute)."""
84
+
85
+ def __init__(self, subscription_id: str, resource_group: str):
86
+ self.subscription_id = subscription_id
87
+ self.resource_group = resource_group
88
+ logger.info(f"Azure provider initialized (subscription: {subscription_id})")
89
+ # TODO: Initialize Azure client
90
+
91
+ def deploy_vm(self, config: Dict[str, Any]) -> str:
92
+ """Deploy Azure VM."""
93
+ logger.info("Deploying Azure VM...")
94
+ # TODO: Implement with azure-mgmt-compute
95
+ return "azure-vm-placeholder"
96
+
97
+ def get_vm_status(self, instance_id: str) -> str:
98
+ """Get Azure VM status."""
99
+ # TODO: Implement
100
+ return "running"
101
+
102
+ def terminate_vm(self, instance_id: str) -> bool:
103
+ """Terminate Azure VM."""
104
+ logger.info(f"Terminating Azure VM: {instance_id}")
105
+ # TODO: Implement
106
+ return True
107
+
108
+
109
+ class MultiCloudManager:
110
+ """Manage deployments across multiple clouds."""
111
+
112
+ def __init__(self):
113
+ self.providers: Dict[str, CloudProvider] = {}
114
+
115
+ def register_provider(self, name: str, provider: CloudProvider):
116
+ """Register a cloud provider."""
117
+ self.providers[name] = provider
118
+ logger.info(f"Registered cloud provider: {name}")
119
+
120
+ def deploy(self, provider_name: str, config: Dict[str, Any]) -> str:
121
+ """Deploy to specified cloud provider."""
122
+ if provider_name not in self.providers:
123
+ raise ValueError(f"Unknown provider: {provider_name}")
124
+
125
+ return self.providers[provider_name].deploy_vm(config)
vm_tool/completion.py ADDED
@@ -0,0 +1,200 @@
1
+ """CLI auto-completion support for bash, zsh, and fish shells."""
2
+
3
+ import os
4
+ import sys
5
+
6
+
7
+ def generate_bash_completion() -> str:
8
+ """Generate bash completion script."""
9
+ return """# vm_tool bash completion
10
+
11
+ _vm_tool_completion() {
12
+ local cur prev opts
13
+ COMPREPLY=()
14
+ cur="${COMP_WORDS[COMP_CWORD]}"
15
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
16
+
17
+ # Main commands
18
+ local commands="config history rollback drift-check backup setup setup-cloud setup-k8s setup-monitoring deploy-docker generate-pipeline --help --version --verbose --debug"
19
+
20
+ # Config subcommands
21
+ local config_commands="set get unset list create-profile list-profiles delete-profile"
22
+
23
+ # Backup subcommands
24
+ local backup_commands="create list restore"
25
+
26
+ if [ $COMP_CWORD -eq 1 ]; then
27
+ COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) )
28
+ return 0
29
+ fi
30
+
31
+ case "${prev}" in
32
+ config)
33
+ COMPREPLY=( $(compgen -W "${config_commands}" -- ${cur}) )
34
+ return 0
35
+ ;;
36
+ backup)
37
+ COMPREPLY=( $(compgen -W "${backup_commands}" -- ${cur}) )
38
+ return 0
39
+ ;;
40
+ --profile)
41
+ # Complete with available profiles
42
+ local profiles=$(vm_tool config list-profiles 2>/dev/null | grep -v "^Available" | awk '{print $1}')
43
+ COMPREPLY=( $(compgen -W "${profiles}" -- ${cur}) )
44
+ return 0
45
+ ;;
46
+ --host|--user|--compose-file)
47
+ # File/path completion
48
+ COMPREPLY=( $(compgen -f -- ${cur}) )
49
+ return 0
50
+ ;;
51
+ esac
52
+ }
53
+
54
+ complete -F _vm_tool_completion vm_tool
55
+ """
56
+
57
+
58
+ def generate_zsh_completion() -> str:
59
+ """Generate zsh completion script."""
60
+ return """#compdef vm_tool
61
+
62
+ _vm_tool() {
63
+ local -a commands
64
+ commands=(
65
+ 'config:Manage configuration'
66
+ 'history:Show deployment history'
67
+ 'rollback:Rollback to previous deployment'
68
+ 'drift-check:Check for configuration drift'
69
+ 'backup:Backup and restore operations'
70
+ 'setup:Setup VM with Docker and deploy'
71
+ 'setup-cloud:Setup cloud VMs'
72
+ 'setup-k8s:Install K3s Kubernetes cluster'
73
+ 'setup-monitoring:Install Prometheus and Grafana'
74
+ 'deploy-docker:Deploy using Docker Compose'
75
+ 'generate-pipeline:Generate CI/CD pipeline configuration'
76
+ )
77
+
78
+ local -a config_commands
79
+ config_commands=(
80
+ 'set:Set a config value'
81
+ 'get:Get a config value'
82
+ 'unset:Unset a config value'
83
+ 'list:List all config values'
84
+ 'create-profile:Create a deployment profile'
85
+ 'list-profiles:List all profiles'
86
+ 'delete-profile:Delete a profile'
87
+ )
88
+
89
+ local -a backup_commands
90
+ backup_commands=(
91
+ 'create:Create a backup'
92
+ 'list:List backups'
93
+ 'restore:Restore a backup'
94
+ )
95
+
96
+ _arguments -C \\
97
+ '1: :->command' \\
98
+ '*::arg:->args'
99
+
100
+ case $state in
101
+ command)
102
+ _describe 'vm_tool commands' commands
103
+ ;;
104
+ args)
105
+ case $words[1] in
106
+ config)
107
+ _describe 'config commands' config_commands
108
+ ;;
109
+ backup)
110
+ _describe 'backup commands' backup_commands
111
+ ;;
112
+ esac
113
+ ;;
114
+ esac
115
+ }
116
+
117
+ _vm_tool "$@"
118
+ """
119
+
120
+
121
+ def generate_fish_completion() -> str:
122
+ """Generate fish completion script."""
123
+ return """# vm_tool fish completion
124
+
125
+ # Main commands
126
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'config' -d 'Manage configuration'
127
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'history' -d 'Show deployment history'
128
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'rollback' -d 'Rollback to previous deployment'
129
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'drift-check' -d 'Check for configuration drift'
130
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'backup' -d 'Backup and restore operations'
131
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'setup' -d 'Setup VM with Docker and deploy'
132
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'setup-cloud' -d 'Setup cloud VMs'
133
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'setup-k8s' -d 'Install K3s Kubernetes cluster'
134
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'setup-monitoring' -d 'Install Prometheus and Grafana'
135
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'deploy-docker' -d 'Deploy using Docker Compose'
136
+ complete -c vm_tool -f -n '__fish_use_subcommand' -a 'generate-pipeline' -d 'Generate CI/CD pipeline'
137
+
138
+ # Config subcommands
139
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from config' -a 'set' -d 'Set a config value'
140
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from config' -a 'get' -d 'Get a config value'
141
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from config' -a 'unset' -d 'Unset a config value'
142
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from config' -a 'list' -d 'List all config values'
143
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from config' -a 'create-profile' -d 'Create a deployment profile'
144
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from config' -a 'list-profiles' -d 'List all profiles'
145
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from config' -a 'delete-profile' -d 'Delete a profile'
146
+
147
+ # Backup subcommands
148
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from backup' -a 'create' -d 'Create a backup'
149
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from backup' -a 'list' -d 'List backups'
150
+ complete -c vm_tool -f -n '__fish_seen_subcommand_from backup' -a 'restore' -d 'Restore a backup'
151
+
152
+ # Global options
153
+ complete -c vm_tool -l help -d 'Show help message'
154
+ complete -c vm_tool -l version -d 'Show version'
155
+ complete -c vm_tool -s v -l verbose -d 'Enable verbose output'
156
+ complete -c vm_tool -s d -l debug -d 'Enable debug logging'
157
+ """
158
+
159
+
160
+ def install_completion(shell: str = "bash") -> str:
161
+ """Install completion script for specified shell."""
162
+ if shell == "bash":
163
+ script = generate_bash_completion()
164
+ path = "/etc/bash_completion.d/vm_tool"
165
+ alt_path = os.path.expanduser("~/.bash_completion.d/vm_tool")
166
+ elif shell == "zsh":
167
+ script = generate_zsh_completion()
168
+ path = "/usr/local/share/zsh/site-functions/_vm_tool"
169
+ alt_path = os.path.expanduser("~/.zsh/completion/_vm_tool")
170
+ elif shell == "fish":
171
+ script = generate_fish_completion()
172
+ path = "/usr/share/fish/vendor_completions.d/vm_tool.fish"
173
+ alt_path = os.path.expanduser("~/.config/fish/completions/vm_tool.fish")
174
+ else:
175
+ raise ValueError(f"Unsupported shell: {shell}")
176
+
177
+ # Try system path first, fall back to user path
178
+ try:
179
+ os.makedirs(os.path.dirname(path), exist_ok=True)
180
+ with open(path, "w") as f:
181
+ f.write(script)
182
+ return path
183
+ except PermissionError:
184
+ os.makedirs(os.path.dirname(alt_path), exist_ok=True)
185
+ with open(alt_path, "w") as f:
186
+ f.write(script)
187
+ return alt_path
188
+
189
+
190
+ def print_completion(shell: str = "bash"):
191
+ """Print completion script to stdout."""
192
+ if shell == "bash":
193
+ print(generate_bash_completion())
194
+ elif shell == "zsh":
195
+ print(generate_zsh_completion())
196
+ elif shell == "fish":
197
+ print(generate_fish_completion())
198
+ else:
199
+ print(f"Error: Unsupported shell: {shell}", file=sys.stderr)
200
+ sys.exit(1)
vm_tool/compliance.py ADDED
@@ -0,0 +1,104 @@
1
+ """Compliance scanning and security checks."""
2
+
3
+ import logging
4
+ from typing import List, Dict, Any
5
+ from dataclasses import dataclass
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ @dataclass
11
+ class ComplianceIssue:
12
+ """Compliance issue details."""
13
+
14
+ severity: str # "critical", "high", "medium", "low"
15
+ category: str
16
+ description: str
17
+ remediation: str
18
+
19
+
20
+ class ComplianceScanner:
21
+ """Scan deployments for compliance issues."""
22
+
23
+ def __init__(self):
24
+ self.issues: List[ComplianceIssue] = []
25
+
26
+ def scan_docker_compose(self, compose_file: str) -> List[ComplianceIssue]:
27
+ """Scan docker-compose file for compliance."""
28
+ logger.info(f"Scanning {compose_file} for compliance issues")
29
+ issues = []
30
+
31
+ # TODO: Implement actual scanning
32
+ # - Check for privileged containers
33
+ # - Check for host network mode
34
+ # - Check for exposed sensitive ports
35
+ # - Check for missing resource limits
36
+
37
+ return issues
38
+
39
+ def scan_secrets(self, secrets_config: Dict[str, Any]) -> List[ComplianceIssue]:
40
+ """Scan secrets configuration."""
41
+ logger.info("Scanning secrets configuration")
42
+ issues = []
43
+
44
+ # TODO: Check for weak encryption, exposed secrets, etc.
45
+
46
+ return issues
47
+
48
+ def generate_compliance_report(self) -> str:
49
+ """Generate compliance report."""
50
+ if not self.issues:
51
+ return "✅ No compliance issues found"
52
+
53
+ report = f"Compliance Scan Report\n{'='*50}\n"
54
+ report += f"Total Issues: {len(self.issues)}\n\n"
55
+
56
+ for issue in self.issues:
57
+ report += f"[{issue.severity.upper()}] {issue.category}\n"
58
+ report += f" {issue.description}\n"
59
+ report += f" Remediation: {issue.remediation}\n\n"
60
+
61
+ return report
62
+
63
+
64
+ class CostOptimizer:
65
+ """Cost optimization recommendations."""
66
+
67
+ def analyze_resource_usage(self, metrics: Dict[str, Any]) -> List[str]:
68
+ """Analyze resource usage and provide recommendations."""
69
+ logger.info("Analyzing resource usage for cost optimization")
70
+ recommendations = []
71
+
72
+ # TODO: Implement actual analysis
73
+ # - Check for over-provisioned resources
74
+ # - Identify idle instances
75
+ # - Suggest reserved instances
76
+ # - Recommend spot instances
77
+
78
+ return recommendations
79
+
80
+
81
+ class DisasterRecovery:
82
+ """Disaster recovery automation."""
83
+
84
+ def create_dr_plan(self, config: Dict[str, Any]) -> Dict[str, Any]:
85
+ """Create disaster recovery plan."""
86
+ logger.info("Creating disaster recovery plan")
87
+
88
+ plan = {
89
+ "backup_frequency": "daily",
90
+ "retention_days": 30,
91
+ "failover_region": "us-west-2",
92
+ "rto_minutes": 60, # Recovery Time Objective
93
+ "rpo_minutes": 15, # Recovery Point Objective
94
+ }
95
+
96
+ # TODO: Implement actual DR planning
97
+
98
+ return plan
99
+
100
+ def execute_failover(self, primary_region: str, dr_region: str) -> bool:
101
+ """Execute failover to DR region."""
102
+ logger.info(f"Executing failover: {primary_region} -> {dr_region}")
103
+ # TODO: Implement actual failover
104
+ return True
vm_tool/config.py ADDED
@@ -0,0 +1,92 @@
1
+ """Configuration management for vm_tool."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class Config:
12
+ """Manages vm_tool configuration and profiles."""
13
+
14
+ def __init__(self):
15
+ self.config_dir = Path.home() / ".vm_tool"
16
+ self.config_file = self.config_dir / "config.json"
17
+ self.profiles_file = self.config_dir / "profiles.json"
18
+ self._ensure_config_dir()
19
+
20
+ def _ensure_config_dir(self):
21
+ """Create config directory if it doesn't exist."""
22
+ self.config_dir.mkdir(parents=True, exist_ok=True)
23
+
24
+ def _load(self, file_path: Path) -> Dict[str, Any]:
25
+ """Load JSON config file."""
26
+ if not file_path.exists():
27
+ return {}
28
+ try:
29
+ with open(file_path, "r") as f:
30
+ return json.load(f)
31
+ except json.JSONDecodeError:
32
+ logger.warning(f"Invalid JSON in {file_path}, returning empty config")
33
+ return {}
34
+
35
+ def _save(self, data: Dict[str, Any], file_path: Path):
36
+ """Save data to JSON config file."""
37
+ with open(file_path, "w") as f:
38
+ json.dump(data, f, indent=2)
39
+
40
+ def set(self, key: str, value: Any):
41
+ """Set a configuration value."""
42
+ config = self._load(self.config_file)
43
+ config[key] = value
44
+ self._save(config, self.config_file)
45
+ logger.info(f"Set config: {key} = {value}")
46
+
47
+ def get(self, key: str, default: Any = None) -> Any:
48
+ """Get a configuration value."""
49
+ config = self._load(self.config_file)
50
+ return config.get(key, default)
51
+
52
+ def unset(self, key: str):
53
+ """Remove a configuration value."""
54
+ config = self._load(self.config_file)
55
+ if key in config:
56
+ del config[key]
57
+ self._save(config, self.config_file)
58
+ logger.info(f"Unset config: {key}")
59
+ else:
60
+ logger.warning(f"Config key not found: {key}")
61
+
62
+ def list_all(self) -> Dict[str, Any]:
63
+ """List all configuration values."""
64
+ return self._load(self.config_file)
65
+
66
+ # Profile management
67
+ def create_profile(self, name: str, environment: str = "development", **kwargs):
68
+ """Create a deployment profile with environment tag."""
69
+ profiles = self._load(self.profiles_file)
70
+ profile_data = {"environment": environment, **kwargs}
71
+ profiles[name] = profile_data
72
+ self._save(profiles, self.profiles_file)
73
+ logger.info(f"Created profile: {name} (environment: {environment})")
74
+
75
+ def get_profile(self, name: str) -> Optional[Dict[str, Any]]:
76
+ """Get a deployment profile."""
77
+ profiles = self._load(self.profiles_file)
78
+ return profiles.get(name)
79
+
80
+ def list_profiles(self) -> Dict[str, Dict[str, Any]]:
81
+ """List all profiles."""
82
+ return self._load(self.profiles_file)
83
+
84
+ def delete_profile(self, name: str):
85
+ """Delete a deployment profile."""
86
+ profiles = self._load(self.profiles_file)
87
+ if name in profiles:
88
+ del profiles[name]
89
+ self._save(profiles, self.profiles_file)
90
+ logger.info(f"Deleted profile: {name}")
91
+ else:
92
+ logger.warning(f"Profile not found: {name}")
vm_tool/drift.py ADDED
@@ -0,0 +1,98 @@
1
+ """Drift detection to catch manual server changes."""
2
+
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class DriftDetector:
14
+ """Detects configuration drift on deployed servers."""
15
+
16
+ def __init__(self, state_dir: Optional[Path] = None):
17
+ if state_dir is None:
18
+ state_dir = Path.home() / ".vm_tool"
19
+ self.state_dir = state_dir
20
+ self.drift_file = self.state_dir / "drift_state.json"
21
+ self._ensure_state_dir()
22
+
23
+ def _ensure_state_dir(self):
24
+ """Create state directory if it doesn't exist."""
25
+ self.state_dir.mkdir(parents=True, exist_ok=True)
26
+
27
+ def _load_drift_state(self) -> Dict:
28
+ """Load drift state from file."""
29
+ if not self.drift_file.exists():
30
+ return {}
31
+ try:
32
+ with open(self.drift_file, "r") as f:
33
+ return json.load(f)
34
+ except json.JSONDecodeError:
35
+ logger.warning("Invalid drift state file")
36
+ return {}
37
+
38
+ def _save_drift_state(self, state: Dict):
39
+ """Save drift state to file."""
40
+ with open(self.drift_file, "w") as f:
41
+ json.dump(state, f, indent=2)
42
+
43
+ def record_file_state(self, host: str, file_path: str, content_hash: str):
44
+ """Record the expected state of a file."""
45
+ state = self._load_drift_state()
46
+ if host not in state:
47
+ state[host] = {}
48
+ state[host][file_path] = content_hash
49
+ self._save_drift_state(state)
50
+
51
+ def check_drift(self, host: str, user: str = "ubuntu") -> List[Dict]:
52
+ """Check for drift on a host."""
53
+ state = self._load_drift_state()
54
+ if host not in state:
55
+ logger.info(f"No baseline state for {host}")
56
+ return []
57
+
58
+ drifts = []
59
+ for file_path, expected_hash in state[host].items():
60
+ actual_hash = self._get_remote_file_hash(host, user, file_path)
61
+ if actual_hash and actual_hash != expected_hash:
62
+ drifts.append(
63
+ {
64
+ "file": file_path,
65
+ "expected": expected_hash,
66
+ "actual": actual_hash,
67
+ "status": "modified",
68
+ }
69
+ )
70
+ elif not actual_hash:
71
+ drifts.append(
72
+ {
73
+ "file": file_path,
74
+ "expected": expected_hash,
75
+ "actual": None,
76
+ "status": "deleted",
77
+ }
78
+ )
79
+
80
+ return drifts
81
+
82
+ def _get_remote_file_hash(
83
+ self, host: str, user: str, file_path: str
84
+ ) -> Optional[str]:
85
+ """Get hash of a file on remote server."""
86
+ try:
87
+ result = subprocess.run(
88
+ ["ssh", f"{user}@{host}", f"sha256sum {file_path}"],
89
+ capture_output=True,
90
+ text=True,
91
+ timeout=10,
92
+ )
93
+ if result.returncode == 0:
94
+ # sha256sum output: "hash filename"
95
+ return result.stdout.split()[0]
96
+ except Exception as e:
97
+ logger.error(f"Failed to get remote file hash: {e}")
98
+ return None