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
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
|