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/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()