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/cli.py
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(
|
|
7
|
+
description="VM Tool: Setup, Provision, and Manage VMs"
|
|
8
|
+
)
|
|
9
|
+
parser.add_argument("--version", action="version", version="1.0.32")
|
|
10
|
+
parser.add_argument(
|
|
11
|
+
"--verbose", "-v", action="store_true", help="Enable verbose output"
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--debug", "-d", action="store_true", help="Enable debug logging"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
18
|
+
|
|
19
|
+
# Config command
|
|
20
|
+
config_parser = subparsers.add_parser("config", help="Manage configuration")
|
|
21
|
+
config_subparsers = config_parser.add_subparsers(
|
|
22
|
+
dest="config_command", help="Config operations"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# config set
|
|
26
|
+
set_parser = config_subparsers.add_parser("set", help="Set a config value")
|
|
27
|
+
set_parser.add_argument("key", type=str, help="Config key")
|
|
28
|
+
set_parser.add_argument("value", type=str, help="Config value")
|
|
29
|
+
|
|
30
|
+
# config get
|
|
31
|
+
get_parser = config_subparsers.add_parser("get", help="Get a config value")
|
|
32
|
+
get_parser.add_argument("key", type=str, help="Config key")
|
|
33
|
+
|
|
34
|
+
# config unset
|
|
35
|
+
unset_parser = config_subparsers.add_parser("unset", help="Unset a config value")
|
|
36
|
+
unset_parser.add_argument("key", type=str, help="Config key")
|
|
37
|
+
|
|
38
|
+
# config list
|
|
39
|
+
list_parser = config_subparsers.add_parser("list", help="List all config values")
|
|
40
|
+
|
|
41
|
+
# config create-profile
|
|
42
|
+
create_profile_parser = config_subparsers.add_parser(
|
|
43
|
+
"create-profile", help="Create a deployment profile"
|
|
44
|
+
)
|
|
45
|
+
create_profile_parser.add_argument("name", type=str, help="Profile name")
|
|
46
|
+
create_profile_parser.add_argument(
|
|
47
|
+
"--environment",
|
|
48
|
+
type=str,
|
|
49
|
+
default="development",
|
|
50
|
+
choices=["development", "staging", "production"],
|
|
51
|
+
help="Environment tag for this profile",
|
|
52
|
+
)
|
|
53
|
+
create_profile_parser.add_argument("--host", type=str, help="Target host")
|
|
54
|
+
create_profile_parser.add_argument("--user", type=str, help="SSH user")
|
|
55
|
+
create_profile_parser.add_argument(
|
|
56
|
+
"--compose-file", type=str, help="Docker Compose file"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# config list-profiles
|
|
60
|
+
list_profiles_parser = config_subparsers.add_parser(
|
|
61
|
+
"list-profiles", help="List all profiles"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# config delete-profile
|
|
65
|
+
delete_profile_parser = config_subparsers.add_parser(
|
|
66
|
+
"delete-profile", help="Delete a profile"
|
|
67
|
+
)
|
|
68
|
+
delete_profile_parser.add_argument("name", type=str, help="Profile name")
|
|
69
|
+
|
|
70
|
+
# History command
|
|
71
|
+
history_parser = subparsers.add_parser("history", help="Show deployment history")
|
|
72
|
+
history_parser.add_argument("--host", type=str, help="Filter by host")
|
|
73
|
+
history_parser.add_argument(
|
|
74
|
+
"--limit", type=int, default=10, help="Number of deployments to show"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Rollback command
|
|
78
|
+
rollback_parser = subparsers.add_parser(
|
|
79
|
+
"rollback", help="Rollback to previous deployment"
|
|
80
|
+
)
|
|
81
|
+
rollback_parser.add_argument("--host", type=str, required=True, help="Target host")
|
|
82
|
+
rollback_parser.add_argument(
|
|
83
|
+
"--to", type=str, help="Deployment ID to rollback to (default: previous)"
|
|
84
|
+
)
|
|
85
|
+
rollback_parser.add_argument(
|
|
86
|
+
"--inventory", type=str, default="inventory.yml", help="Inventory file"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Drift check command
|
|
90
|
+
drift_parser = subparsers.add_parser(
|
|
91
|
+
"drift-check", help="Check for configuration drift on server"
|
|
92
|
+
)
|
|
93
|
+
drift_parser.add_argument(
|
|
94
|
+
"--host", type=str, required=True, help="Target host to check"
|
|
95
|
+
)
|
|
96
|
+
drift_parser.add_argument("--user", type=str, default="ubuntu", help="SSH user")
|
|
97
|
+
|
|
98
|
+
# Backup commands
|
|
99
|
+
backup_parser = subparsers.add_parser(
|
|
100
|
+
"backup", help="Backup and restore operations"
|
|
101
|
+
)
|
|
102
|
+
backup_subparsers = backup_parser.add_subparsers(dest="backup_command")
|
|
103
|
+
|
|
104
|
+
# backup create
|
|
105
|
+
create_backup_parser = backup_subparsers.add_parser(
|
|
106
|
+
"create", help="Create a backup"
|
|
107
|
+
)
|
|
108
|
+
create_backup_parser.add_argument("--host", type=str, required=True)
|
|
109
|
+
create_backup_parser.add_argument("--user", type=str, default="ubuntu")
|
|
110
|
+
create_backup_parser.add_argument(
|
|
111
|
+
"--paths", type=str, nargs="+", required=True, help="Paths to backup"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# backup list
|
|
115
|
+
list_backup_parser = backup_subparsers.add_parser("list", help="List backups")
|
|
116
|
+
list_backup_parser.add_argument("--host", type=str, help="Filter by host")
|
|
117
|
+
|
|
118
|
+
# backup restore
|
|
119
|
+
restore_backup_parser = backup_subparsers.add_parser(
|
|
120
|
+
"restore", help="Restore a backup"
|
|
121
|
+
)
|
|
122
|
+
restore_backup_parser.add_argument(
|
|
123
|
+
"--id", type=str, required=True, help="Backup ID"
|
|
124
|
+
)
|
|
125
|
+
restore_backup_parser.add_argument("--host", type=str, required=True)
|
|
126
|
+
restore_backup_parser.add_argument("--user", type=str, default="ubuntu")
|
|
127
|
+
# VM Setup command
|
|
128
|
+
setup_parser = subparsers.add_parser(
|
|
129
|
+
"setup", help="Setup VM with Docker and deploy"
|
|
130
|
+
)
|
|
131
|
+
setup_parser.add_argument("--github-username", type=str)
|
|
132
|
+
setup_parser.add_argument("--github-token", type=str)
|
|
133
|
+
setup_parser.add_argument("--github-project-url", type=str, required=True)
|
|
134
|
+
setup_parser.add_argument("--github-branch", type=str, default="main")
|
|
135
|
+
setup_parser.add_argument(
|
|
136
|
+
"--docker-compose-file-path", type=str, default="docker-compose.yml"
|
|
137
|
+
)
|
|
138
|
+
setup_parser.add_argument("--dockerhub-username", type=str)
|
|
139
|
+
setup_parser.add_argument("--dockerhub-password", type=str)
|
|
140
|
+
|
|
141
|
+
# Cloud Setup command
|
|
142
|
+
setup_cloud_parser = subparsers.add_parser("setup-cloud", help="Setup cloud VMs")
|
|
143
|
+
setup_cloud_parser.add_argument(
|
|
144
|
+
"--ssh-configs", type=str, required=True, help="Path to SSH configs JSON file"
|
|
145
|
+
)
|
|
146
|
+
setup_cloud_parser.add_argument("--github-username", type=str)
|
|
147
|
+
setup_cloud_parser.add_argument("--github-token", type=str)
|
|
148
|
+
setup_cloud_parser.add_argument("--github-project-url", type=str, required=True)
|
|
149
|
+
setup_cloud_parser.add_argument("--github-branch", type=str, default="main")
|
|
150
|
+
setup_cloud_parser.add_argument(
|
|
151
|
+
"--docker-compose-file-path", type=str, default="docker-compose.yml"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# K8s Setup command
|
|
155
|
+
k8s_parser = subparsers.add_parser(
|
|
156
|
+
"setup-k8s", help="Install K3s Kubernetes cluster"
|
|
157
|
+
)
|
|
158
|
+
k8s_parser.add_argument(
|
|
159
|
+
"--inventory", type=str, default="inventory.yml", help="Inventory file to use"
|
|
160
|
+
)
|
|
161
|
+
# Reuse SetupRunnerConfig arguments if needed, but for now we assume inventory is enough or environment vars
|
|
162
|
+
|
|
163
|
+
# Monitoring Setup command
|
|
164
|
+
mon_parser = subparsers.add_parser(
|
|
165
|
+
"setup-monitoring", help="Install Prometheus and Grafana"
|
|
166
|
+
)
|
|
167
|
+
mon_parser.add_argument(
|
|
168
|
+
"--inventory", type=str, default="inventory.yml", help="Inventory file to use"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Docker Deploy command
|
|
172
|
+
docker_parser = subparsers.add_parser(
|
|
173
|
+
"deploy-docker", help="Deploy using Docker Compose"
|
|
174
|
+
)
|
|
175
|
+
docker_parser.add_argument(
|
|
176
|
+
"--inventory", type=str, default="inventory.yml", help="Inventory file to use"
|
|
177
|
+
)
|
|
178
|
+
docker_parser.add_argument(
|
|
179
|
+
"--compose-file",
|
|
180
|
+
type=str,
|
|
181
|
+
default="docker-compose.yml",
|
|
182
|
+
help="Path to docker-compose.yml",
|
|
183
|
+
)
|
|
184
|
+
docker_parser.add_argument(
|
|
185
|
+
"--host", type=str, help="Target host IP/domain (generates dynamic inventory)"
|
|
186
|
+
)
|
|
187
|
+
docker_parser.add_argument("--user", type=str, help="SSH username for target host")
|
|
188
|
+
docker_parser.add_argument(
|
|
189
|
+
"--env-file", type=str, help="Path to env file (optional)"
|
|
190
|
+
)
|
|
191
|
+
docker_parser.add_argument(
|
|
192
|
+
"--webhook-url", type=str, help="Webhook URL for deployment notifications"
|
|
193
|
+
)
|
|
194
|
+
docker_parser.add_argument(
|
|
195
|
+
"--notify-email",
|
|
196
|
+
type=str,
|
|
197
|
+
action="append",
|
|
198
|
+
help="Email addresses for notifications (can be specified multiple times)",
|
|
199
|
+
)
|
|
200
|
+
docker_parser.add_argument(
|
|
201
|
+
"--smtp-host", type=str, default="localhost", help="SMTP server host"
|
|
202
|
+
)
|
|
203
|
+
docker_parser.add_argument(
|
|
204
|
+
"--smtp-port", type=int, default=587, help="SMTP server port"
|
|
205
|
+
)
|
|
206
|
+
docker_parser.add_argument(
|
|
207
|
+
"--deploy-command",
|
|
208
|
+
type=str,
|
|
209
|
+
help="Custom deployment command (overrides default docker compose up)",
|
|
210
|
+
)
|
|
211
|
+
docker_parser.add_argument(
|
|
212
|
+
"--profile", type=str, help="Use a saved deployment profile"
|
|
213
|
+
)
|
|
214
|
+
docker_parser.add_argument(
|
|
215
|
+
"--force",
|
|
216
|
+
action="store_true",
|
|
217
|
+
help="Force redeployment even if no changes detected",
|
|
218
|
+
)
|
|
219
|
+
docker_parser.add_argument(
|
|
220
|
+
"--health-check",
|
|
221
|
+
type=str,
|
|
222
|
+
help="Health check command to run after deployment (e.g., 'curl http://localhost:8000/health')",
|
|
223
|
+
)
|
|
224
|
+
docker_parser.add_argument(
|
|
225
|
+
"--health-port",
|
|
226
|
+
type=int,
|
|
227
|
+
help="Port to check for availability after deployment",
|
|
228
|
+
)
|
|
229
|
+
docker_parser.add_argument(
|
|
230
|
+
"--health-url",
|
|
231
|
+
type=str,
|
|
232
|
+
help="HTTP URL to check after deployment (e.g., 'http://localhost:8000/health')",
|
|
233
|
+
)
|
|
234
|
+
docker_parser.add_argument(
|
|
235
|
+
"--dry-run",
|
|
236
|
+
action="store_true",
|
|
237
|
+
help="Preview deployment without executing (shows what would be deployed)",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Completion command
|
|
241
|
+
completion_parser = subparsers.add_parser(
|
|
242
|
+
"completion", help="Generate shell completion scripts"
|
|
243
|
+
)
|
|
244
|
+
completion_parser.add_argument(
|
|
245
|
+
"shell",
|
|
246
|
+
type=str,
|
|
247
|
+
choices=["bash", "zsh", "fish"],
|
|
248
|
+
help="Shell type",
|
|
249
|
+
)
|
|
250
|
+
completion_parser.add_argument(
|
|
251
|
+
"--install",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="Install completion script",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Pipeline Generator command
|
|
257
|
+
pipe_parser = subparsers.add_parser(
|
|
258
|
+
"generate-pipeline", help="Generate CI/CD pipeline configuration"
|
|
259
|
+
)
|
|
260
|
+
pipe_parser.add_argument(
|
|
261
|
+
"--platform",
|
|
262
|
+
type=str,
|
|
263
|
+
default="github",
|
|
264
|
+
choices=["github"],
|
|
265
|
+
help="CI/CD Platform",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
args = parser.parse_args()
|
|
269
|
+
|
|
270
|
+
# Configure logging based on flags
|
|
271
|
+
import logging
|
|
272
|
+
|
|
273
|
+
if args.debug:
|
|
274
|
+
logging.basicConfig(
|
|
275
|
+
level=logging.DEBUG,
|
|
276
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
277
|
+
)
|
|
278
|
+
print("š Debug logging enabled")
|
|
279
|
+
elif args.verbose:
|
|
280
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
281
|
+
print("š¢ Verbose output enabled")
|
|
282
|
+
else:
|
|
283
|
+
logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
|
|
284
|
+
|
|
285
|
+
if args.command == "config":
|
|
286
|
+
from vm_tool.config import Config
|
|
287
|
+
|
|
288
|
+
config = Config()
|
|
289
|
+
|
|
290
|
+
if args.config_command == "set":
|
|
291
|
+
config.set(args.key, args.value)
|
|
292
|
+
print(f"ā
Set {args.key} = {args.value}")
|
|
293
|
+
|
|
294
|
+
elif args.config_command == "get":
|
|
295
|
+
value = config.get(args.key)
|
|
296
|
+
if value is not None:
|
|
297
|
+
print(f"{args.key} = {value}")
|
|
298
|
+
else:
|
|
299
|
+
print(f"ā Config key '{args.key}' not found")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
elif args.config_command == "unset":
|
|
303
|
+
config.unset(args.key)
|
|
304
|
+
print(f"ā
Unset {args.key}")
|
|
305
|
+
|
|
306
|
+
elif args.config_command == "list":
|
|
307
|
+
all_config = config.list_all()
|
|
308
|
+
if all_config:
|
|
309
|
+
print("š Configuration:")
|
|
310
|
+
for key, value in all_config.items():
|
|
311
|
+
print(f" {key} = {value}")
|
|
312
|
+
else:
|
|
313
|
+
print("No configuration set")
|
|
314
|
+
|
|
315
|
+
elif args.config_command == "create-profile":
|
|
316
|
+
profile_data = {}
|
|
317
|
+
if args.host:
|
|
318
|
+
profile_data["host"] = args.host
|
|
319
|
+
if args.user:
|
|
320
|
+
profile_data["user"] = args.user
|
|
321
|
+
if args.compose_file:
|
|
322
|
+
profile_data["compose_file"] = args.compose_file
|
|
323
|
+
|
|
324
|
+
config.create_profile(
|
|
325
|
+
args.name, environment=args.environment, **profile_data
|
|
326
|
+
)
|
|
327
|
+
print(f"ā
Created profile '{args.name}' (environment: {args.environment})")
|
|
328
|
+
|
|
329
|
+
elif args.config_command == "list-profiles":
|
|
330
|
+
profiles = config.list_profiles()
|
|
331
|
+
if profiles:
|
|
332
|
+
print("š Profiles:")
|
|
333
|
+
for name, data in profiles.items():
|
|
334
|
+
print(f" {name}:")
|
|
335
|
+
for key, value in data.items():
|
|
336
|
+
print(f" {key} = {value}")
|
|
337
|
+
else:
|
|
338
|
+
print("No profiles configured")
|
|
339
|
+
|
|
340
|
+
elif args.config_command == "delete-profile":
|
|
341
|
+
config.delete_profile(args.name)
|
|
342
|
+
print(f"ā
Deleted profile '{args.name}'")
|
|
343
|
+
|
|
344
|
+
else:
|
|
345
|
+
config_parser.print_help()
|
|
346
|
+
|
|
347
|
+
elif args.command == "history":
|
|
348
|
+
from vm_tool.history import DeploymentHistory
|
|
349
|
+
|
|
350
|
+
history = DeploymentHistory()
|
|
351
|
+
deployments = history.get_history(host=args.host, limit=args.limit)
|
|
352
|
+
|
|
353
|
+
if not deployments:
|
|
354
|
+
print("No deployment history found")
|
|
355
|
+
else:
|
|
356
|
+
print(
|
|
357
|
+
f"\nš Deployment History (showing {len(deployments)} deployments):\n"
|
|
358
|
+
)
|
|
359
|
+
for dep in deployments:
|
|
360
|
+
status_icon = "ā
" if dep.get("status") == "success" else "ā"
|
|
361
|
+
print(f"{status_icon} {dep['id']} - {dep['timestamp']}")
|
|
362
|
+
print(f" Host: {dep['host']}")
|
|
363
|
+
print(f" Service: {dep.get('service_name', 'default')}")
|
|
364
|
+
print(f" Compose: {dep['compose_file']}")
|
|
365
|
+
if dep.get("git_commit"):
|
|
366
|
+
print(f" Git: {dep['git_commit'][:8]}")
|
|
367
|
+
if dep.get("error"):
|
|
368
|
+
print(f" Error: {dep['error']}")
|
|
369
|
+
print()
|
|
370
|
+
|
|
371
|
+
elif args.command == "rollback":
|
|
372
|
+
from vm_tool.history import DeploymentHistory
|
|
373
|
+
from vm_tool.runner import SetupRunner, SetupRunnerConfig
|
|
374
|
+
|
|
375
|
+
history = DeploymentHistory()
|
|
376
|
+
rollback_info = history.get_rollback_info(args.host, args.to)
|
|
377
|
+
|
|
378
|
+
if not rollback_info:
|
|
379
|
+
print(f"ā No deployment found to rollback to")
|
|
380
|
+
sys.exit(1)
|
|
381
|
+
|
|
382
|
+
print(f"\nš Rolling back to deployment: {rollback_info['id']}")
|
|
383
|
+
print(f" Timestamp: {rollback_info['timestamp']}")
|
|
384
|
+
print(f" Compose: {rollback_info['compose_file']}")
|
|
385
|
+
if rollback_info.get("git_commit"):
|
|
386
|
+
print(f" Git commit: {rollback_info['git_commit'][:8]}")
|
|
387
|
+
|
|
388
|
+
confirm = input("\nProceed with rollback? (yes/no): ").strip().lower()
|
|
389
|
+
if confirm != "yes":
|
|
390
|
+
print("ā Rollback cancelled")
|
|
391
|
+
sys.exit(0)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
config = SetupRunnerConfig(github_project_url="dummy")
|
|
395
|
+
runner = SetupRunner(config)
|
|
396
|
+
runner.run_docker_deploy(
|
|
397
|
+
compose_file=rollback_info["compose_file"],
|
|
398
|
+
inventory_file=args.inventory,
|
|
399
|
+
host=args.host,
|
|
400
|
+
user=None, # Will use inventory
|
|
401
|
+
force=True, # Force redeployment
|
|
402
|
+
)
|
|
403
|
+
print("\nā
Rollback completed successfully")
|
|
404
|
+
except Exception as e:
|
|
405
|
+
print(f"\nā Rollback failed: {e}")
|
|
406
|
+
sys.exit(1)
|
|
407
|
+
|
|
408
|
+
elif args.command == "drift-check":
|
|
409
|
+
from vm_tool.drift import DriftDetector
|
|
410
|
+
|
|
411
|
+
detector = DriftDetector()
|
|
412
|
+
drifts = detector.check_drift(args.host, args.user)
|
|
413
|
+
|
|
414
|
+
if not drifts:
|
|
415
|
+
print(f"ā
No drift detected on {args.host}")
|
|
416
|
+
else:
|
|
417
|
+
print(f"\nā ļø Drift Detected on {args.host}:\n")
|
|
418
|
+
for drift in drifts:
|
|
419
|
+
status_icon = "š" if drift["status"] == "modified" else "ā"
|
|
420
|
+
print(f"{status_icon} {drift['file']}")
|
|
421
|
+
print(f" Status: {drift['status']}")
|
|
422
|
+
print(f" Expected: {drift['expected'][:16]}...")
|
|
423
|
+
if drift["actual"]:
|
|
424
|
+
print(f" Actual: {drift['actual'][:16]}...")
|
|
425
|
+
print()
|
|
426
|
+
print(f"Found {len(drifts)} file(s) with drift")
|
|
427
|
+
sys.exit(1)
|
|
428
|
+
|
|
429
|
+
elif args.command == "backup":
|
|
430
|
+
from vm_tool.backup import BackupManager
|
|
431
|
+
|
|
432
|
+
manager = BackupManager()
|
|
433
|
+
|
|
434
|
+
if args.backup_command == "create":
|
|
435
|
+
try:
|
|
436
|
+
backup_id = manager.create_backup(
|
|
437
|
+
host=args.host, user=args.user, paths=args.paths
|
|
438
|
+
)
|
|
439
|
+
print(f"ā
Backup created: {backup_id}")
|
|
440
|
+
except Exception as e:
|
|
441
|
+
print(f"ā Backup failed: {e}")
|
|
442
|
+
sys.exit(1)
|
|
443
|
+
|
|
444
|
+
elif args.backup_command == "list":
|
|
445
|
+
backups = manager.list_backups(host=args.host)
|
|
446
|
+
if not backups:
|
|
447
|
+
print("No backups found")
|
|
448
|
+
else:
|
|
449
|
+
print(f"\\nš¦ Available Backups ({len(backups)}):\\n")
|
|
450
|
+
for backup in backups:
|
|
451
|
+
size_mb = backup["size"] / (1024 * 1024)
|
|
452
|
+
print(f" {backup['id']}")
|
|
453
|
+
print(f" Host: {backup['host']}")
|
|
454
|
+
print(f" Time: {backup['timestamp']}")
|
|
455
|
+
print(f" Size: {size_mb:.2f} MB")
|
|
456
|
+
print(f" Paths: {', '.join(backup['paths'])}")
|
|
457
|
+
print()
|
|
458
|
+
|
|
459
|
+
elif args.backup_command == "restore":
|
|
460
|
+
try:
|
|
461
|
+
manager.restore_backup(args.id, args.host, args.user)
|
|
462
|
+
print(f"ā
Backup restored: {args.id}")
|
|
463
|
+
except Exception as e:
|
|
464
|
+
print(f"ā Restore failed: {e}")
|
|
465
|
+
sys.exit(1)
|
|
466
|
+
|
|
467
|
+
elif args.command == "setup":
|
|
468
|
+
from vm_tool.runner import SetupRunner, SetupRunnerConfig
|
|
469
|
+
|
|
470
|
+
config = SetupRunnerConfig(
|
|
471
|
+
github_username=args.github_username,
|
|
472
|
+
github_token=args.github_token,
|
|
473
|
+
github_project_url=args.github_project_url,
|
|
474
|
+
github_branch=args.github_branch,
|
|
475
|
+
docker_compose_file_path=args.docker_compose_file_path,
|
|
476
|
+
dockerhub_username=args.dockerhub_username,
|
|
477
|
+
dockerhub_password=args.dockerhub_password,
|
|
478
|
+
)
|
|
479
|
+
runner = SetupRunner(config)
|
|
480
|
+
runner.run_setup()
|
|
481
|
+
print("ā
VM setup complete!")
|
|
482
|
+
|
|
483
|
+
elif args.command == "setup-cloud":
|
|
484
|
+
import json
|
|
485
|
+
from vm_tool.runner import SetupRunner, SetupRunnerConfig, SSHConfig
|
|
486
|
+
|
|
487
|
+
config = SetupRunnerConfig(
|
|
488
|
+
github_username=args.github_username,
|
|
489
|
+
github_token=args.github_token,
|
|
490
|
+
github_project_url=args.github_project_url,
|
|
491
|
+
github_branch=args.github_branch,
|
|
492
|
+
docker_compose_file_path=args.docker_compose_file_path,
|
|
493
|
+
)
|
|
494
|
+
runner = SetupRunner(config)
|
|
495
|
+
|
|
496
|
+
# Load SSH configs from JSON file
|
|
497
|
+
with open(args.ssh_configs, "r") as f:
|
|
498
|
+
ssh_data = json.load(f)
|
|
499
|
+
|
|
500
|
+
ssh_configs = [SSHConfig(**cfg) for cfg in ssh_data]
|
|
501
|
+
runner.run_cloud_setup(ssh_configs)
|
|
502
|
+
print("ā
Cloud setup complete!")
|
|
503
|
+
|
|
504
|
+
elif args.command == "setup-k8s":
|
|
505
|
+
try:
|
|
506
|
+
# We need a dummy config to init SetupRunner, or refactor SetupRunner to be more flexible.
|
|
507
|
+
# For now, we pass minimal required args.
|
|
508
|
+
from vm_tool.runner import SetupRunner, SetupRunnerConfig
|
|
509
|
+
|
|
510
|
+
config = SetupRunnerConfig(github_project_url="dummy")
|
|
511
|
+
runner = SetupRunner(config)
|
|
512
|
+
runner.run_k8s_setup(inventory_file=args.inventory)
|
|
513
|
+
except Exception as e:
|
|
514
|
+
print(f"Error: {e}")
|
|
515
|
+
sys.exit(1)
|
|
516
|
+
|
|
517
|
+
elif args.command == "setup-monitoring":
|
|
518
|
+
try:
|
|
519
|
+
from vm_tool.runner import SetupRunner, SetupRunnerConfig
|
|
520
|
+
|
|
521
|
+
config = SetupRunnerConfig(github_project_url="dummy")
|
|
522
|
+
runner = SetupRunner(config)
|
|
523
|
+
runner.run_monitoring_setup(inventory_file=args.inventory)
|
|
524
|
+
except Exception as e:
|
|
525
|
+
print(f"Error: {e}")
|
|
526
|
+
sys.exit(1)
|
|
527
|
+
|
|
528
|
+
elif args.command == "deploy-docker":
|
|
529
|
+
try:
|
|
530
|
+
from vm_tool.config import Config
|
|
531
|
+
from vm_tool.runner import SetupRunner, SetupRunnerConfig
|
|
532
|
+
|
|
533
|
+
# Load profile if specified
|
|
534
|
+
profile_data = {}
|
|
535
|
+
if args.profile:
|
|
536
|
+
config = Config()
|
|
537
|
+
profile_data = config.get_profile(args.profile) or {}
|
|
538
|
+
if not profile_data:
|
|
539
|
+
print(f"ā Profile '{args.profile}' not found")
|
|
540
|
+
sys.exit(1)
|
|
541
|
+
print(f"š Using profile: {args.profile}")
|
|
542
|
+
|
|
543
|
+
# Safety check for production deployments
|
|
544
|
+
if profile_data.get("environment") == "production":
|
|
545
|
+
if not args.force:
|
|
546
|
+
confirm = (
|
|
547
|
+
input(
|
|
548
|
+
"ā ļø You are deploying to PRODUCTION. Type 'yes' to confirm: "
|
|
549
|
+
)
|
|
550
|
+
.strip()
|
|
551
|
+
.lower()
|
|
552
|
+
)
|
|
553
|
+
if confirm != "yes":
|
|
554
|
+
print("ā Deployment cancelled")
|
|
555
|
+
sys.exit(0)
|
|
556
|
+
|
|
557
|
+
# Merge profile with CLI args (CLI args take precedence)
|
|
558
|
+
host = args.host or profile_data.get("host")
|
|
559
|
+
user = args.user or profile_data.get("user")
|
|
560
|
+
compose_file = args.compose_file or profile_data.get(
|
|
561
|
+
"compose_file", "docker-compose.yml"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Dry-run mode: show what would be deployed
|
|
565
|
+
if args.dry_run:
|
|
566
|
+
print("\nš DRY-RUN MODE - No changes will be made\n")
|
|
567
|
+
print(f"š Deployment Plan:")
|
|
568
|
+
print(f" Target Host: {host or 'from inventory'}")
|
|
569
|
+
print(f" SSH User: {user or 'from inventory'}")
|
|
570
|
+
print(f" Compose File: {compose_file}")
|
|
571
|
+
print(f" Inventory: {args.inventory}")
|
|
572
|
+
if args.env_file:
|
|
573
|
+
print(f" Env File: {args.env_file}")
|
|
574
|
+
if args.deploy_command:
|
|
575
|
+
print(f" Custom Command: {args.deploy_command}")
|
|
576
|
+
|
|
577
|
+
# Show compose file contents
|
|
578
|
+
import os
|
|
579
|
+
|
|
580
|
+
if os.path.exists(compose_file):
|
|
581
|
+
print(f"\nš Compose File Contents ({compose_file}):")
|
|
582
|
+
with open(compose_file, "r") as f:
|
|
583
|
+
for i, line in enumerate(f, 1):
|
|
584
|
+
print(f" {i:3d} | {line.rstrip()}")
|
|
585
|
+
else:
|
|
586
|
+
print(f"\nā ļø Compose file not found: {compose_file}")
|
|
587
|
+
|
|
588
|
+
print(f"\nā
Dry-run complete. Use without --dry-run to deploy.")
|
|
589
|
+
sys.exit(0)
|
|
590
|
+
|
|
591
|
+
# For deployment, we might need github creds if the playbook pulls code
|
|
592
|
+
# But for now we use dummy or env vars
|
|
593
|
+
config = SetupRunnerConfig(github_project_url="dummy")
|
|
594
|
+
runner = SetupRunner(config)
|
|
595
|
+
runner.run_docker_deploy(
|
|
596
|
+
compose_file=compose_file,
|
|
597
|
+
inventory_file=args.inventory,
|
|
598
|
+
host=host,
|
|
599
|
+
user=user,
|
|
600
|
+
env_file=args.env_file,
|
|
601
|
+
deploy_command=args.deploy_command,
|
|
602
|
+
force=args.force,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Run health checks if specified
|
|
606
|
+
if host and (args.health_check or args.health_port or args.health_url):
|
|
607
|
+
from vm_tool.health import SmokeTestSuite
|
|
608
|
+
|
|
609
|
+
print("\nš„ Running Health Checks...")
|
|
610
|
+
suite = SmokeTestSuite(host)
|
|
611
|
+
|
|
612
|
+
if args.health_port:
|
|
613
|
+
suite.add_port_check(args.health_port)
|
|
614
|
+
|
|
615
|
+
if args.health_url:
|
|
616
|
+
suite.add_http_check(args.health_url)
|
|
617
|
+
|
|
618
|
+
if args.health_check:
|
|
619
|
+
suite.add_custom_check(args.health_check, "Custom Health Check")
|
|
620
|
+
|
|
621
|
+
if not suite.run_all():
|
|
622
|
+
print(
|
|
623
|
+
"\nā Health checks failed. Deployment may not be working correctly."
|
|
624
|
+
)
|
|
625
|
+
sys.exit(1)
|
|
626
|
+
else:
|
|
627
|
+
print("\nā
All health checks passed!")
|
|
628
|
+
|
|
629
|
+
except Exception as e:
|
|
630
|
+
print(f"Error: {e}")
|
|
631
|
+
sys.exit(1)
|
|
632
|
+
|
|
633
|
+
elif args.command == "completion":
|
|
634
|
+
from vm_tool.completion import print_completion, install_completion
|
|
635
|
+
|
|
636
|
+
if args.install:
|
|
637
|
+
try:
|
|
638
|
+
path = install_completion(args.shell)
|
|
639
|
+
print(f"ā
Completion installed: {path}")
|
|
640
|
+
print(f"\nTo activate, run:")
|
|
641
|
+
if args.shell == "bash":
|
|
642
|
+
print(f" source {path}")
|
|
643
|
+
elif args.shell == "zsh":
|
|
644
|
+
print(
|
|
645
|
+
f" # Add to ~/.zshrc: fpath=({os.path.dirname(path)} $fpath)"
|
|
646
|
+
)
|
|
647
|
+
print(f" # Then run: compinit")
|
|
648
|
+
elif args.shell == "fish":
|
|
649
|
+
print(f" # Restart your shell or run: source {path}")
|
|
650
|
+
except Exception as e:
|
|
651
|
+
print(f"ā Failed to install completion: {e}")
|
|
652
|
+
sys.exit(1)
|
|
653
|
+
else:
|
|
654
|
+
print_completion(args.shell)
|
|
655
|
+
|
|
656
|
+
elif args.command == "generate-pipeline":
|
|
657
|
+
try:
|
|
658
|
+
from vm_tool.generator import PipelineGenerator
|
|
659
|
+
|
|
660
|
+
print("š Configuring your CI/CD Pipeline...")
|
|
661
|
+
|
|
662
|
+
# Interactive prompts
|
|
663
|
+
branch = (
|
|
664
|
+
input("Enter the branch to trigger deployment [main]: ").strip()
|
|
665
|
+
or "main"
|
|
666
|
+
)
|
|
667
|
+
python_version = input("Enter Python version [3.12]: ").strip() or "3.12"
|
|
668
|
+
|
|
669
|
+
enable_linting = (
|
|
670
|
+
input("Include Linting step (flake8)? [y/N]: ").strip().lower()
|
|
671
|
+
)
|
|
672
|
+
run_linting = enable_linting in ("y", "yes")
|
|
673
|
+
|
|
674
|
+
enable_tests = (
|
|
675
|
+
input("Include Testing step (pytest)? [y/N]: ").strip().lower()
|
|
676
|
+
)
|
|
677
|
+
run_tests = enable_tests in ("y", "yes")
|
|
678
|
+
|
|
679
|
+
enable_monitoring = (
|
|
680
|
+
input("Include Monitoring (Prometheus/Grafana)? [y/N]: ")
|
|
681
|
+
.strip()
|
|
682
|
+
.lower()
|
|
683
|
+
)
|
|
684
|
+
setup_monitoring = enable_monitoring in ("y", "yes")
|
|
685
|
+
|
|
686
|
+
dep_type_input = (
|
|
687
|
+
input("Deployment Type (docker/registry/custom) [docker]: ")
|
|
688
|
+
.strip()
|
|
689
|
+
.lower()
|
|
690
|
+
)
|
|
691
|
+
deployment_type = "docker"
|
|
692
|
+
strategy = "docker"
|
|
693
|
+
|
|
694
|
+
if dep_type_input in ("custom", "c"):
|
|
695
|
+
deployment_type = "custom"
|
|
696
|
+
strategy = "custom"
|
|
697
|
+
elif dep_type_input in ("registry", "ghcr", "r"):
|
|
698
|
+
deployment_type = "docker"
|
|
699
|
+
strategy = "registry"
|
|
700
|
+
elif dep_type_input in ("kubernetes", "k8s", "k"):
|
|
701
|
+
deployment_type = "kubernetes"
|
|
702
|
+
strategy = "kubernetes"
|
|
703
|
+
|
|
704
|
+
docker_compose_file = "docker-compose.yml"
|
|
705
|
+
env_file = None
|
|
706
|
+
deploy_command = None
|
|
707
|
+
|
|
708
|
+
if deployment_type == "custom":
|
|
709
|
+
deploy_command = input("Enter custom deployment command: ").strip()
|
|
710
|
+
|
|
711
|
+
elif deployment_type == "docker":
|
|
712
|
+
docker_compose_file = (
|
|
713
|
+
input(
|
|
714
|
+
"Enter Docker Compose file name [docker-compose.yml]: "
|
|
715
|
+
).strip()
|
|
716
|
+
or "docker-compose.yml"
|
|
717
|
+
)
|
|
718
|
+
env_file_input = input(
|
|
719
|
+
"Enter Env file path (optional, press Enter to skip): "
|
|
720
|
+
).strip()
|
|
721
|
+
if env_file_input:
|
|
722
|
+
env_file = env_file_input
|
|
723
|
+
|
|
724
|
+
context = {
|
|
725
|
+
"branch_name": branch,
|
|
726
|
+
"python_version": python_version,
|
|
727
|
+
"run_linting": run_linting,
|
|
728
|
+
"run_tests": run_tests,
|
|
729
|
+
"setup_monitoring": setup_monitoring,
|
|
730
|
+
"deployment_type": deployment_type,
|
|
731
|
+
"docker_compose_file": docker_compose_file,
|
|
732
|
+
"env_file": env_file,
|
|
733
|
+
"deploy_command": deploy_command,
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
# Use new generator API
|
|
737
|
+
generator = PipelineGenerator(
|
|
738
|
+
platform=args.platform,
|
|
739
|
+
strategy=strategy,
|
|
740
|
+
enable_monitoring=setup_monitoring,
|
|
741
|
+
)
|
|
742
|
+
generator.set_options(
|
|
743
|
+
run_linting=run_linting,
|
|
744
|
+
run_tests=run_tests,
|
|
745
|
+
python_version=python_version,
|
|
746
|
+
branch=branch,
|
|
747
|
+
)
|
|
748
|
+
output = generator.generate()
|
|
749
|
+
|
|
750
|
+
# Save to file
|
|
751
|
+
output_path = generator.save()
|
|
752
|
+
print(f"ā
Deployment pipeline generated: {output_path}")
|
|
753
|
+
except Exception as e:
|
|
754
|
+
print(f"Error: {e}")
|
|
755
|
+
sys.exit(1)
|
|
756
|
+
else:
|
|
757
|
+
parser.print_help()
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
if __name__ == "__main__":
|
|
761
|
+
main()
|