1use anyhow::{Context, Result};
2use console::style;
3use dialoguer::{Confirm, Input, MultiSelect, Select};
4use indicatif::{ProgressBar, ProgressStyle};
5use libomni::types::db::v1 as types;
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::Path;
9use tabled::{Table, Tabled};
10use tokio::time::Duration;
11
12use crate::ui::PremiumUI;
13
14#[derive(Debug, Deserialize)]
15struct ApiResponse {
16 status: String,
17 message: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 data: Option<serde_json::Value>,
20}
21
22#[derive(Debug, Serialize, Deserialize, Clone)]
23pub struct SshHost {
24 name: String,
25 hostname: String,
26 username: String,
27 password: Option<String>,
28 port: u16,
29 identity_file: Option<String>,
30 is_bastion: bool,
31}
32
33#[derive(Debug, Serialize, Deserialize)]
34pub struct CloudConfig {
35 company_name: String,
36 admin_name: String,
37 cloud_name: String,
38 region: String,
39 ssh_hosts: Vec<SshHost>,
40 enable_monitoring: bool,
41 enable_backups: bool,
42 backup_retention_days: u32,
43}
44
45#[derive(Debug, Clone, Deserialize)]
46pub struct HostDeploymentStatus {
47 host: String,
48 status: String,
49 services: Vec<ServiceStatus>,
50 current_step: String,
51 progress: u8,
52 error: Option<String>,
53 completed: bool,
54}
55
56#[derive(Debug, Clone, Deserialize)]
57pub struct ServiceStatus {
58 name: String,
59 status: String,
60 uptime: Option<String>,
61 cpu: Option<String>,
62 memory: Option<String>,
63}
64
65#[derive(Tabled)]
66struct SshHostDisplay {
67 #[tabled(rename = "Name")]
68 name: String,
69 #[tabled(rename = "Hostname")]
70 hostname: String,
71 #[tabled(rename = "Username")]
72 username: String,
73 #[tabled(rename = "Password")]
74 password: String,
75 #[tabled(rename = "Port")]
76 port: String,
77 #[tabled(rename = "Identity File")]
78 identity_file: String,
79 #[tabled(rename = "Bastion")]
80 is_bastion: String,
81}
82
83#[derive(Tabled)]
84struct ServiceStatusDisplay {
85 #[tabled(rename = "Host")]
86 host: String,
87 #[tabled(rename = "Service")]
88 service: String,
89 #[tabled(rename = "Status")]
90 status: String,
91 #[tabled(rename = "Uptime")]
92 uptime: String,
93 #[tabled(rename = "CPU")]
94 cpu: String,
95 #[tabled(rename = "Memory")]
96 memory: String,
97}
98
99impl From<&SshHost> for SshHostDisplay {
100 fn from(host: &SshHost) -> Self {
101 SshHostDisplay {
102 name: host.name.clone(),
103 hostname: host.hostname.clone(),
104 username: host.username.clone(),
105 password: "***".to_string(),
106 port: host.port.to_string(),
107 identity_file: host
108 .identity_file
109 .clone()
110 .unwrap_or_else(|| "-".to_string()),
111 is_bastion: if host.is_bastion { "Yes" } else { "No" }.to_string(),
112 }
113 }
114}
115
116impl PremiumUI {
117 pub async fn init_environment(&self) -> Result<()> {
118 let config_dir = "config";
119 let config_path = format!("{}/cloud-config.json", config_dir);
120 let config = if Path::new(&config_path).exists() {
121 println!(
122 "\n{}",
123 style("📋 Using existing configuration").cyan().bold()
124 );
125 let config_json =
126 fs::read_to_string(&config_path).context("Failed to read configuration file")?;
127 let config: CloudConfig =
128 serde_json::from_str(&config_json).context("Failed to parse configuration")?;
129
130 println!("Company: {}", style(&config.company_name).green());
132 println!("Cloud Name: {}", style(&config.cloud_name).green());
133 println!("SSH Hosts: {}", style(config.ssh_hosts.len()).green());
134
135 config
136 } else {
137 println!(
138 "\n{}",
139 style("🚀 Cloud Environment Configuration").cyan().bold()
140 );
141 println!(
142 "{}",
143 style("This wizard will help you configure your self-hosted cloud environment.")
144 .dim()
145 );
146
147 let company_name: String = Input::with_theme(&self.theme)
149 .with_prompt("Company name")
150 .interact_text()?;
151
152 let admin_name: String = Input::with_theme(&self.theme)
153 .with_prompt("Your name (admin)")
154 .interact_text()?;
155
156 let cloud_name: String = Input::with_theme(&self.theme)
157 .with_prompt("Cloud platform name")
158 .default(format!(
159 "{}-cloud",
160 company_name.to_lowercase().replace(" ", "-")
161 ))
162 .interact_text()?;
163
164 println!("{}", style("Fetching available regions...").dim());
166 let regions_response = match self.api_client.get::<Vec<types::region::Region>>("/regions").await {
167 Ok(response) => {
168 response
169 },
170 Err(err) => {
171 println!("{}", style("Failed to fetch regions from API").red());
172 println!("{}", style(format!("Error: {:?}", err)).red());
173 return Err(anyhow::anyhow!("Failed to fetch regions from API: {}", err));
174 }
175 };
176
177 if regions_response.is_empty() {
178 println!("{}", style("No regions found. Using default region.").yellow());
179 } else {
180 println!(
181 "{}",
182 style(format!("Found {} regions", regions_response.len())).green()
183 );
184 }
185
186 let mut regions: Vec<String> = regions_response
188 .iter()
189 .map(|r| r.name.clone())
191 .collect();
192 regions.push("custom".to_string());
193 let region_selection = Select::with_theme(&self.theme)
194 .with_prompt("Select primary region")
195 .items(®ions)
196 .default(0)
197 .interact()?;
198
199 let region = if regions[region_selection] == "custom" {
200 Input::with_theme(&self.theme)
201 .with_prompt("Enter custom region")
202 .interact_text()?
203 } else {
204 regions[region_selection].to_string()
205 };
206
207 let mut ssh_hosts = Vec::new();
209 println!("\n{}", style("📡 SSH Host Configuration").cyan().bold());
210 println!(
211 "{}",
212 style("Configure SSH hosts for your cloud environment").dim()
213 );
214
215 loop {
216 if !ssh_hosts.is_empty() {
218 println!("\n{}", style("Current SSH Hosts:").cyan());
219
220 let display_hosts: Vec<SshHostDisplay> =
221 ssh_hosts.iter().map(SshHostDisplay::from).collect();
222
223 let table = Table::new(display_hosts).to_string();
224 println!("{}", table);
225 }
226
227 let add_host = Confirm::with_theme(&self.theme)
229 .with_prompt("Would you like to add an SSH host?")
230 .default(true)
231 .interact()?;
232
233 if !add_host {
234 break;
235 }
236
237 let host_name: String = Input::with_theme(&self.theme)
239 .with_prompt("Host name (identifier)")
240 .interact_text()?;
241
242 let hostname: String = Input::with_theme(&self.theme)
243 .with_prompt("Hostname or IP address")
244 .interact_text()?;
245
246 let username: String = Input::with_theme(&self.theme)
247 .with_prompt("SSH username")
248 .default("root".into())
249 .interact_text()?;
250
251 let port: u16 = Input::with_theme(&self.theme)
252 .with_prompt("SSH port")
253 .default(22)
254 .interact_text()?;
255
256 let use_identity_file = Confirm::with_theme(&self.theme)
257 .with_prompt("Use identity file for authentication? (If no you will be prompted for the password)")
258 .default(true)
259 .interact()?;
260
261 let mut identity_file: Option<String> = None;
262 let mut password: Option<String> = None;
263 if use_identity_file {
264 identity_file = Some(
265 Input::with_theme(&self.theme)
266 .with_prompt("Path to identity file")
267 .default("~/.ssh/id_rsa".into())
268 .interact_text()?,
269 );
270 } else {
271 let input_password = Input::with_theme(&self.theme)
272 .with_prompt("SSH password")
273 .default("".into())
274 .interact_text()?;
275 password = Some(input_password);
276 };
277
278 let is_bastion = Confirm::with_theme(&self.theme)
279 .with_prompt("Is this a bastion/jump host?")
280 .default(false)
281 .interact()?;
282
283 ssh_hosts.push(SshHost {
285 name: host_name,
286 hostname,
287 username,
288 password,
289 port,
290 identity_file,
291 is_bastion,
292 });
293
294 println!("{}", style("✅ SSH host added successfully").green());
295 }
296
297 println!("\n{}", style("⚙️ Additional Configuration").cyan().bold());
299
300 let options = vec!["Enable system monitoring", "Enable automated backups"];
301 let defaults = vec![true, true];
302
303 let selections = MultiSelect::with_theme(&self.theme)
304 .with_prompt("Select additional services to enable")
305 .items(&options)
306 .defaults(&defaults)
307 .interact()?;
308
309 let enable_monitoring = selections.contains(&0);
310 let enable_backups = selections.contains(&1);
311
312 let backup_retention_days = if enable_backups {
313 Input::with_theme(&self.theme)
314 .with_prompt("Backup retention period (days)")
315 .default(30)
316 .interact_text()?
317 } else {
318 7 };
320
321 let config = CloudConfig {
323 company_name,
324 admin_name,
325 cloud_name,
326 region,
327 ssh_hosts,
328 enable_monitoring,
329 enable_backups,
330 backup_retention_days,
331 };
332
333 println!("\n{}", style("💾 Saving Configuration").cyan().bold());
335
336 if !Path::new(config_dir).exists() {
337 fs::create_dir(config_dir).context("Failed to create config directory")?;
338 }
339
340 let config_json = serde_json::to_string_pretty(&config)?;
341 fs::write(&config_path, config_json).context("Failed to write configuration file")?;
342
343 println!(
344 "{}",
345 style(format!("✅ Configuration saved to {}", config_path)).green()
346 );
347
348 println!("\n{}", style("📊 Configuration Summary").cyan().bold());
350 println!("Company: {}", style(&config.company_name).green());
351 println!("Admin: {}", style(&config.admin_name).green());
352 println!("Cloud Name: {}", style(&config.cloud_name).green());
353 println!("Region: {}", style(&config.region).green());
354 println!("SSH Hosts: {}", style(config.ssh_hosts.len()).green());
355 println!(
356 "Monitoring: {}",
357 if config.enable_monitoring {
358 style("Enabled").green()
359 } else {
360 style("Disabled").yellow()
361 }
362 );
363 println!(
364 "Backups: {}",
365 if config.enable_backups {
366 style("Enabled").green()
367 } else {
368 style("Disabled").yellow()
369 }
370 );
371
372 if config.enable_backups {
373 println!(
374 "Backup Retention: {} days",
375 style(config.backup_retention_days).green()
376 );
377 }
378
379 config
380 };
381
382 println!(
384 "\n{}",
385 style("⚡ Bootstrapping OmniOrchestrator").cyan().bold()
386 );
387 println!(
388 "{}",
389 style(format!(
390 "Setting up OmniOrchestrator for {} cloud environment",
391 config.cloud_name
392 ))
393 .dim()
394 );
395
396 if config.ssh_hosts.is_empty() {
398 println!(
399 "{}",
400 style("No SSH hosts configured. Cannot bootstrap OmniOrchestrator.").yellow()
401 );
402 return Ok(());
403 }
404
405 let confirm = Confirm::with_theme(&self.theme)
407 .with_prompt("Ready to bootstrap OmniOrchestrator on all configured hosts?")
408 .default(true)
409 .interact()?;
410
411 if !confirm {
412 println!("{}", style("Bootstrapping cancelled.").yellow());
413 return Ok(());
414 }
415
416 self.bootstrap_orchestrator(&config).await?;
418
419 println!(
420 "\n{}",
421 style("✨ Environment initialization completed!")
422 .green()
423 .bold()
424 );
425 println!(
426 "{}",
427 style("Your OmniOrchestrator cloud environment is ready.").dim()
428 );
429 println!(
430 "{}",
431 style("You can now deploy applications with 'omni deploy'.").dim()
432 );
433
434 Ok(())
435 }
436
437 async fn bootstrap_orchestrator(&self, config: &CloudConfig) -> Result<()> {
438 println!(
439 "\n{}",
440 style(format!(
441 "Initializing platform with {} hosts...",
442 config.ssh_hosts.len()
443 ))
444 .cyan()
445 );
446
447 println!("{}", style("Sending configuration to API...").cyan());
449
450 let api_config = CloudConfig {
452 company_name: config.company_name.clone(),
453 admin_name: config.admin_name.clone(),
454 cloud_name: config.cloud_name.clone(),
455 region: config.region.clone(),
456 ssh_hosts: config.ssh_hosts.clone(),
457 enable_monitoring: config.enable_monitoring,
458 enable_backups: config.enable_backups,
459 backup_retention_days: config.backup_retention_days,
460 };
461
462 match self
463 .api_client
464 .post::<_, ApiResponse>("/platforms/init", &api_config)
465 .await
466 {
467 Err(err) => {
468 println!("{}", style("API initialization failed").red().bold());
469 println!("{}", style(format!("Error: {:?}", err)).red());
470 return Err(anyhow::anyhow!("Failed to initialize platform: {:?}", err));
471 }
472 Ok(response) => {
473 println!("{}", style("Configuration sent successfully ✓").green());
474 println!(
475 "{}",
476 style(format!("API response: {}", response.message)).green()
477 );
478 }
479 }
480
481 let mut all_complete = false;
483 let cloud_name = &config.cloud_name;
484
485 println!(
486 "\n{}",
487 style("Monitoring deployment progress:").cyan().bold()
488 );
489
490 let mut prev_lines = 0;
491 while !all_complete {
492 match self
493 .api_client
494 .get::<ApiResponse>(&format!("/platforms/{}/status", cloud_name))
495 .await
496 {
497 Err(err) => {
498 println!(
499 "{}",
500 style("Failed to get deployment status: ").red().bold()
501 );
502 println!("{}", style(format!("{:?}", err)).red());
503 tokio::time::sleep(Duration::from_secs(2)).await;
505 }
506 Ok(response) => {
507 if response.status == "completed" {
508 all_complete = true;
509 continue;
510 }
511
512 if let Some(data) = response.data {
514 if let Ok(host_statuses) =
515 serde_json::from_value::<Vec<HostDeploymentStatus>>(data)
516 {
517 if prev_lines > 0 {
519 print!("\x1B[{}A\x1B[J", prev_lines);
520 }
521
522 println!("{}", style("Current deployment status:").cyan());
524 for host in &host_statuses {
525 let status_color = match host.status.as_str() {
526 "completed" => {
527 style(format!("[✓] {}: {}", host.host, host.current_step))
528 .green()
529 }
530 "in_progress" => {
531 style(format!("[↻] {}: {}", host.host, host.current_step))
532 .yellow()
533 }
534 "pending" => {
535 style(format!("[⌛] {}: Waiting", host.host)).dim()
536 }
537 "error" => style(format!(
538 "[✗] {}: Error - {}",
539 host.host,
540 host.error.as_ref().unwrap_or(&"Unknown error".to_string())
541 ))
542 .red(),
543 _ => style(format!("[-] {}: {}", host.host, host.current_step))
544 .dim(),
545 };
546
547 let progress_bar = if host.status == "completed" {
548 "██████████".to_string()
549 } else {
550 let filled = (host.progress as usize) / 10;
551 let empty = 10 - filled;
552 format!("{}{}", "█".repeat(filled), "░".repeat(empty))
553 };
554
555 println!("{} {}% {}", status_color, host.progress, progress_bar);
556 }
557
558 println!(
559 "Overall: {}",
560 style(format!(
561 "{}%",
562 response
563 .message
564 .split_whitespace()
565 .nth(3)
566 .unwrap_or("0")
567 .trim_end_matches('%')
568 ))
569 .cyan()
570 );
571
572 prev_lines = host_statuses.len() + 2;
574 }
575 }
576
577 tokio::time::sleep(Duration::from_secs(1)).await;
579 }
580 }
581 }
582
583 println!("\n{}", style("🔄 Configuring cluster networking").cyan());
585
586 match self
587 .api_client
588 .post::<_, ApiResponse>(&format!("/platforms/{}/network/configure", cloud_name), &())
589 .await
590 {
591 Err(err) => {
592 println!("{}", style("Network configuration failed ✗").red().bold());
593 println!("{}", style(format!("Error: {:?}", err)).red());
594 return Err(anyhow::anyhow!("Failed to configure network: {:?}", err));
595 }
596 Ok(response) => {
597 println!("{}", style("Network configuration initiated ✓").green());
598 println!(
599 "{}",
600 style(format!("API response: {}", response.message)).green()
601 );
602
603 self.wait_for_process_completion(cloud_name, "network")
605 .await?;
606 }
607 }
608
609 if config.enable_monitoring {
611 println!("\n{}", style("📊 Setting up monitoring services").cyan());
612
613 match self
614 .api_client
615 .post::<_, ApiResponse>(&format!("/platforms/{}/monitoring/setup", cloud_name), &())
616 .await
617 {
618 Err(err) => {
619 println!("{}", style("Monitoring setup failed ✗").red().bold());
620 println!("{}", style(format!("Error: {:?}", err)).red());
621 return Err(anyhow::anyhow!("Failed to setup monitoring: {:?}", err));
622 }
623 Ok(response) => {
624 println!("{}", style("Monitoring setup initiated ✓").green());
625 println!(
626 "{}",
627 style(format!("API response: {}", response.message)).green()
628 );
629
630 self.wait_for_process_completion(cloud_name, "monitoring")
632 .await?;
633 }
634 }
635 }
636
637 if config.enable_backups {
639 println!("\n{}", style("💾 Configuring backup services").cyan());
640
641 match self
642 .api_client
643 .post::<_, ApiResponse>(&format!("/platforms/{}/backups/setup", cloud_name), &())
644 .await
645 {
646 Err(err) => {
647 println!("{}", style("Backup setup failed ✗").red().bold());
648 println!("{}", style(format!("Error: {:?}", err)).red());
649 return Err(anyhow::anyhow!("Failed to setup backups: {:?}", err));
650 }
651 Ok(response) => {
652 println!("{}", style("Backup setup initiated ✓").green());
653 println!(
654 "{}",
655 style(format!("API response: {}", response.message)).green()
656 );
657
658 self.wait_for_process_completion(cloud_name, "backups")
660 .await?;
661 }
662 }
663 }
664
665 println!(
666 "{}",
667 style("\nEnvironment is now fully configured and ready to use! ✓")
668 .green()
669 .bold()
670 );
671 Ok(())
672 }
673
674 async fn wait_for_process_completion(
676 &self,
677 cloud_name: &str,
678 process_type: &str,
679 ) -> Result<()> {
680 let mut complete = false;
681 let mut attempts = 0;
682 const MAX_ATTEMPTS: usize = 120; println!(
685 "{}",
686 style(format!("Waiting for {} setup to complete...", process_type)).dim()
687 );
688
689 while !complete && attempts < MAX_ATTEMPTS {
690 attempts += 1;
691
692 match self
693 .api_client
694 .get::<ApiResponse>(&format!("/platforms/{}/status", cloud_name))
695 .await
696 {
697 Ok(response) => {
698 if response.status == "completed" {
700 complete = true;
701 println!(
702 "{}",
703 style(format!("{} setup completed ✓", process_type)).green()
704 );
705 break;
706 }
707
708 if let Some(data) = response.data {
710 if let Ok(host_statuses) =
711 serde_json::from_value::<Vec<HostDeploymentStatus>>(data)
712 {
713 match process_type {
715 "network" => {
716 let network_complete = host_statuses.iter().all(|h| {
718 h.current_step.contains("Network configuration complete")
719 || h.current_step.contains("network") && h.completed
720 });
721
722 if network_complete {
723 complete = true;
724 println!(
725 "{}",
726 style("Network configuration completed ✓").green()
727 );
728 break;
729 }
730
731 if let Some(host) = host_statuses.first() {
733 println!(
734 "{}",
735 style(format!("Network setup: {}", host.current_step))
736 .dim()
737 );
738 }
739 }
740 "monitoring" => {
741 let monitoring_ready = host_statuses.iter().all(|h| {
743 h.services.iter().any(|s| {
744 s.name == "metrics-collector" && s.status == "Running"
745 })
746 });
747
748 if monitoring_ready {
749 complete = true;
750 println!(
751 "{}",
752 style("Monitoring services deployed ✓").green()
753 );
754 break;
755 }
756
757 if let Some(host) = host_statuses
759 .iter()
760 .find(|h| h.current_step.contains("monitoring"))
761 {
762 println!(
763 "{}",
764 style(format!(
765 "Monitoring setup: {}",
766 host.current_step
767 ))
768 .dim()
769 );
770 }
771 }
772 "backups" => {
773 let backups_ready = host_statuses
775 .iter()
776 .filter(|h| {
777 h.services.iter().any(|s| s.name == "backup-manager")
780 })
781 .all(|h| {
782 h.services.iter().any(|s| {
783 s.name == "backup-manager" && s.status == "Running"
784 })
785 });
786
787 if backups_ready {
788 complete = true;
789 println!(
790 "{}",
791 style("Backup services configured ✓").green()
792 );
793 break;
794 }
795
796 if let Some(host) = host_statuses
798 .iter()
799 .find(|h| h.current_step.contains("backup"))
800 {
801 println!(
802 "{}",
803 style(format!("Backup setup: {}", host.current_step))
804 .dim()
805 );
806 }
807 }
808 _ => {
809 if host_statuses.iter().all(|h| h.completed) {
811 complete = true;
812 println!(
813 "{}",
814 style(format!("{} process completed ✓", process_type))
815 .green()
816 );
817 break;
818 }
819 }
820 }
821 }
822 }
823 }
824 Err(err) => {
825 println!(
826 "{}",
827 style(format!("Error polling status: {:?}", err)).yellow()
828 );
829 }
830 }
831
832 tokio::time::sleep(Duration::from_secs(1)).await;
833 }
834
835 if !complete {
836 println!("{}", style(format!("Timed out waiting for {} to complete. The process may still be running on the server.", process_type)).yellow());
837 }
838
839 Ok(())
840 } pub async fn list_ssh_hosts(&self) -> Result<()> {
844 let config_path = "config/cloud-config.json";
845
846 if !Path::new(config_path).exists() {
847 println!(
848 "{}",
849 style("No cloud configuration found. Run 'omni init' first.").yellow()
850 );
851 return Ok(());
852 }
853
854 let config_json =
855 fs::read_to_string(config_path).context("Failed to read configuration file")?;
856 let config: CloudConfig =
857 serde_json::from_str(&config_json).context("Failed to parse configuration")?;
858
859 if config.ssh_hosts.is_empty() {
860 println!(
861 "{}",
862 style("No SSH hosts configured. Run 'omni init' to add hosts.").yellow()
863 );
864 return Ok(());
865 }
866
867 println!("\n{}", style("📡 Configured SSH Hosts").cyan().bold());
868 println!(
869 "Cloud: {} ({})",
870 style(&config.cloud_name).green(),
871 &config.region
872 );
873
874 match self
876 .api_client
877 .get::<ApiResponse>(&format!("/platforms/{}/status", config.cloud_name))
878 .await
879 {
880 Err(err) => {
881 println!("{}", style("Failed to get status from API.").red());
882 println!("{}", style(format!("Error: {:?}", err)).dim());
883 return Err(anyhow::anyhow!("Failed to get status from API: {:?}", err));
884 }
885 Ok(response) => {
886 if let Some(data) = response.data {
887 if let Ok(host_statuses) =
888 serde_json::from_value::<Vec<HostDeploymentStatus>>(data)
889 {
890 self.display_service_status(&host_statuses, &config);
892 } else {
893 println!(
894 "{}",
895 style("Failed to parse host status data from API.").red()
896 );
897 return Err(anyhow::anyhow!("Failed to parse host status data"));
898 }
899 } else {
900 println!("{}", style("No status data available from API.").yellow());
901 return Err(anyhow::anyhow!("No status data available from API"));
902 }
903 }
904 }
905
906 println!("\n{}", style("💡 Available Commands").cyan().bold());
907 println!(
908 "- {}: Restart a service",
909 style("omni service restart <host> <service>").yellow()
910 );
911 println!(
912 "- {}: View detailed logs",
913 style("omni logs <host> <service>").yellow()
914 );
915 println!(
916 "- {}: Trigger immediate backup",
917 style("omni backup now").yellow()
918 );
919
920 Ok(())
921 }
922
923 fn display_service_status(
925 &self,
926 host_statuses: &Vec<HostDeploymentStatus>,
927 config: &CloudConfig,
928 ) {
929 let mut services_display = Vec::new();
930
931 for host_status in host_statuses {
932 for service in &host_status.services {
933 services_display.push(ServiceStatusDisplay {
934 host: host_status.host.clone(),
935 service: service.name.clone(),
936 status: service.status.clone(),
937 uptime: service.uptime.clone().unwrap_or_else(|| "-".to_string()),
938 cpu: service.cpu.clone().unwrap_or_else(|| "-".to_string()),
939 memory: service.memory.clone().unwrap_or_else(|| "-".to_string()),
940 });
941 }
942 }
943
944 if services_display.is_empty() {
945 println!("{}", style("No services found.").yellow());
946 } else {
947 let table = Table::new(services_display).to_string();
948 println!("{}", table);
949 }
950
951 println!("\n{}", style("🔄 System Information").cyan().bold());
952 println!(
953 "Monitoring: {}",
954 if config.enable_monitoring {
955 style("Enabled").green()
956 } else {
957 style("Disabled").yellow()
958 }
959 );
960 println!(
961 "Backups: {}",
962 if config.enable_backups {
963 style("Enabled").green()
964 } else {
965 style("Disabled").yellow()
966 }
967 );
968 if config.enable_backups {
969 println!(
970 " Retention: {} days",
971 style(config.backup_retention_days).green()
972 );
973
974 for host_status in host_statuses {
976 let is_bastion = config
977 .ssh_hosts
978 .iter()
979 .any(|h| h.name == host_status.host && h.is_bastion);
980
981 if is_bastion {
982 if let Some(backup_service) = host_status
983 .services
984 .iter()
985 .find(|s| s.name == "backup-manager")
986 {
987 println!(" Last Backup: {}", style("From server data").green());
989 println!(" Next Backup: {}", style("From server data").green());
990 break;
991 }
992 }
993 }
994 }
995 }
996
997 pub async fn restart_service(&self, host_name: &str, service_name: &str) -> Result<()> {
999 let config_path = "config/cloud-config.json";
1000 let config_json =
1001 fs::read_to_string(config_path).context("Failed to read configuration file")?;
1002 let config: CloudConfig =
1003 serde_json::from_str(&config_json).context("Failed to parse configuration")?;
1004
1005 println!(
1006 "\n{}",
1007 style(format!(
1008 "🔄 Restarting service {} on host {}",
1009 service_name, host_name
1010 ))
1011 .cyan()
1012 .bold()
1013 );
1014
1015 match self
1016 .api_client
1017 .post::<_, ApiResponse>(
1018 &format!(
1019 "/platforms/{}/hosts/{}/services/{}/restart",
1020 config.cloud_name, host_name, service_name
1021 ),
1022 &(),
1023 )
1024 .await
1025 {
1026 Err(err) => {
1027 println!("{}", style("Failed to restart service: ").red().bold());
1028 println!("{}", style(format!("{:?}", err)).red());
1029 return Err(anyhow::anyhow!("Failed to restart service: {:?}", err));
1030 }
1031 Ok(response) => {
1032 println!("{}", style("Restart request sent successfully ✓").green());
1033 println!(
1034 "{}",
1035 style(format!("API response: {}", response.message)).green()
1036 );
1037
1038 println!("{}", style("Waiting for service to restart...").dim());
1040
1041 self.wait_for_service_restart(&config.cloud_name, host_name, service_name)
1042 .await?;
1043 }
1044 }
1045
1046 Ok(())
1047 }
1048
1049 async fn wait_for_service_restart(
1051 &self,
1052 cloud_name: &str,
1053 host_name: &str,
1054 service_name: &str,
1055 ) -> Result<()> {
1056 let mut service_restarted = false;
1057 let mut attempts = 0;
1058 const MAX_ATTEMPTS: usize = 30;
1059
1060 while !service_restarted && attempts < MAX_ATTEMPTS {
1061 attempts += 1;
1062
1063 match self
1064 .api_client
1065 .get::<ApiResponse>(&format!(
1066 "/platforms/{}/hosts/{}/services",
1067 cloud_name, host_name
1068 ))
1069 .await
1070 {
1071 Ok(response) => {
1072 if let Some(data) = response.data {
1073 if let Ok(services) = serde_json::from_value::<Vec<ServiceStatus>>(data) {
1074 if let Some(service) = services.iter().find(|s| s.name == service_name)
1075 {
1076 match service.status.as_str() {
1078 "Running" => {
1079 service_restarted = true;
1080 println!(
1081 "{}",
1082 style("Service restarted successfully! ✓")
1083 .green()
1084 .bold()
1085 );
1086 break;
1087 }
1088 "Restarting" => {
1089 println!(
1090 "{}",
1091 style("Service is currently restarting...").yellow()
1092 );
1093 }
1094 status => {
1095 println!(
1096 "{}",
1097 style(format!("Service status: {}", status)).yellow()
1098 );
1099 }
1100 }
1101 } else {
1102 println!(
1103 "{}",
1104 style(format!("Service '{}' not found on host", service_name))
1105 .yellow()
1106 );
1107 }
1108 }
1109 }
1110 }
1111 Err(err) => {
1112 println!(
1113 "{}",
1114 style(format!("Error checking service status: {:?}", err)).yellow()
1115 );
1116 }
1117 }
1118
1119 tokio::time::sleep(Duration::from_secs(1)).await;
1120 }
1121
1122 if !service_restarted {
1123 println!("{}", style("Timed out waiting for service to restart. The service may still be restarting.").yellow());
1124 }
1125
1126 Ok(())
1127 }
1128
1129 pub async fn view_service_logs(&self, host_name: &str, service_name: &str) -> Result<()> {
1131 let config_path = "config/cloud-config.json";
1132 let config_json =
1133 fs::read_to_string(config_path).context("Failed to read configuration file")?;
1134 let config: CloudConfig =
1135 serde_json::from_str(&config_json).context("Failed to parse configuration")?;
1136
1137 println!(
1138 "\n{}",
1139 style(format!(
1140 "📜 Logs for service {} on host {}",
1141 service_name, host_name
1142 ))
1143 .cyan()
1144 .bold()
1145 );
1146
1147 match self
1148 .api_client
1149 .get::<ApiResponse>(&format!(
1150 "/platforms/{}/hosts/{}/services/{}/logs",
1151 config.cloud_name, host_name, service_name
1152 ))
1153 .await
1154 {
1155 Err(err) => {
1156 println!("{}", style("Failed to retrieve logs: ").red().bold());
1157 println!("{}", style(format!("{:?}", err)).red());
1158 return Err(anyhow::anyhow!("Failed to retrieve logs: {:?}", err));
1159 }
1160 Ok(response) => {
1161 if let Some(data) = response.data {
1162 if let Ok(logs) = serde_json::from_value::<Vec<String>>(data) {
1163 if logs.is_empty() {
1164 println!("{}", style("No logs available for this service.").yellow());
1165 } else {
1166 println!("\n{}", style("Service Logs:").yellow().bold());
1167 for log_line in logs {
1168 let formatted_line = if log_line.contains("[INFO]") {
1169 style(log_line).dim()
1170 } else if log_line.contains("[WARN]") {
1171 style(log_line).yellow()
1172 } else if log_line.contains("[ERROR]") {
1173 style(log_line).red()
1174 } else {
1175 style(log_line)
1176 };
1177
1178 println!("{}", formatted_line);
1179 }
1180 }
1181 } else {
1182 println!("{}", style("Failed to parse log data from API.").red());
1183 return Err(anyhow::anyhow!("Failed to parse log data"));
1184 }
1185 } else {
1186 println!("{}", style("No log data available from API.").yellow());
1187 return Err(anyhow::anyhow!("No log data available"));
1188 }
1189 }
1190 }
1191
1192 println!("\n{}", style("💡 Tip").cyan().bold());
1193 println!(
1194 "Use {} to follow logs in real-time",
1195 style("omni logs <host> <service> --follow").yellow()
1196 );
1197
1198 Ok(())
1199 }
1200
1201 pub async fn trigger_backup(&self) -> Result<()> {
1203 let config_path = "config/cloud-config.json";
1204 let config_json =
1205 fs::read_to_string(config_path).context("Failed to read configuration file")?;
1206 let config: CloudConfig =
1207 serde_json::from_str(&config_json).context("Failed to parse configuration")?;
1208
1209 if !config.enable_backups {
1210 println!(
1211 "{}",
1212 style("Backups are not enabled for this cloud environment.").yellow()
1213 );
1214 return Ok(());
1215 }
1216
1217 println!(
1218 "\n{}",
1219 style("💾 Triggering immediate backup").cyan().bold()
1220 );
1221
1222 match self
1223 .api_client
1224 .post::<_, ApiResponse>(
1225 &format!("/platforms/{}/backups/trigger", config.cloud_name),
1226 &(),
1227 )
1228 .await
1229 {
1230 Err(err) => {
1231 println!("{}", style("Failed to trigger backup: ").red().bold());
1232 println!("{}", style(format!("{:?}", err)).red());
1233 return Err(anyhow::anyhow!("Failed to trigger backup: {:?}", err));
1234 }
1235 Ok(response) => {
1236 println!("{}", style("Backup process initiated ✓").green());
1237 println!(
1238 "{}",
1239 style(format!("API response: {}", response.message)).green()
1240 );
1241
1242 self.wait_for_backup_completion(&config.cloud_name).await?;
1244 }
1245 }
1246
1247 Ok(())
1248 }
1249
1250 async fn wait_for_backup_completion(&self, cloud_name: &str) -> Result<()> {
1252 let mut backup_completed = false;
1253 let mut attempts = 0;
1254 const MAX_ATTEMPTS: usize = 60; println!("{}", style("Monitoring backup progress...").dim());
1257
1258 while !backup_completed && attempts < MAX_ATTEMPTS {
1259 attempts += 1;
1260
1261 match self
1262 .api_client
1263 .get::<ApiResponse>(&format!("/platforms/{}/backups/status", cloud_name))
1264 .await
1265 {
1266 Ok(response) => {
1267 if response.status == "completed" {
1268 backup_completed = true;
1269 println!(
1270 "{}",
1271 style("Backup completed successfully! ✓").green().bold()
1272 );
1273
1274 if let Some(data) = response.data {
1276 if let Ok(backup_info) =
1277 serde_json::from_value::<serde_json::Value>(data)
1278 {
1279 println!("{}", style("Backup Information:").cyan());
1281 if let Some(timestamp) =
1282 backup_info.get("timestamp").and_then(|v| v.as_str())
1283 {
1284 println!("Timestamp: {}", style(timestamp).green());
1285 }
1286 if let Some(size) = backup_info.get("size").and_then(|v| v.as_str())
1287 {
1288 println!("Size: {}", style(size).green());
1289 }
1290 }
1291 }
1292
1293 break;
1294 } else {
1295 if let Some(data) = response.data {
1297 if let Ok(backup_info) =
1298 serde_json::from_value::<serde_json::Value>(data)
1299 {
1300 if let Some(progress) =
1301 backup_info.get("progress").and_then(|v| v.as_u64())
1302 {
1303 println!("Backup progress: {}%", style(progress).cyan());
1304 }
1305 if let Some(current_step) =
1306 backup_info.get("current_step").and_then(|v| v.as_str())
1307 {
1308 println!("Current step: {}", style(current_step).dim());
1309 }
1310 }
1311 } else {
1312 println!("Waiting for backup progress update...");
1313 }
1314 }
1315 }
1316 Err(err) => {
1317 println!(
1318 "{}",
1319 style(format!("Error checking backup status: {:?}", err)).yellow()
1320 );
1321 }
1322 }
1323
1324 tokio::time::sleep(Duration::from_secs(1)).await;
1325 }
1326
1327 if !backup_completed {
1328 println!("{}", style("Timed out waiting for backup to complete. The backup may still be in progress.").yellow());
1329 }
1330
1331 Ok(())
1332 }
1333}