omni/commands/
init_env.rs

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            // Display summary of loaded configuration
131            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            // Basic cloud platform configuration
148            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            // Fetch regions from API
165            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            // Create list of region names from API response
187            let mut regions: Vec<String> = regions_response
188                .iter()
189            //    .filter(|r| r.status == "active")
190                .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(&regions)
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            // SSH hosts configuration
208            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                // Display current hosts if any exist
217                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                // Ask if user wants to add a host
228                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                // Host details
238                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                // Add the host to our list
284                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            // Additional configuration options
298            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 // Default value if backups are not enabled
319            };
320
321            // Create configuration object
322            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            // Save configuration
334            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            // Summary
349            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        // Begin the bootstrapping process
383        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        // Check if there are SSH hosts configured
397        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        // Confirm before proceeding
406        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        // Bootstrap the orchestrator using server-driven approach
417        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        // STEP 1: Initialize the platform by sending configuration to API
448        println!("{}", style("Sending configuration to API...").cyan());
449
450        // Make the API call to init the platform with the provided config
451        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        // STEP 2: Poll for platform status until complete
482        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                    // Wait before retrying
504                    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                    // Extract host statuses from response data
513                    if let Some(data) = response.data {
514                        if let Ok(host_statuses) =
515                            serde_json::from_value::<Vec<HostDeploymentStatus>>(data)
516                        {
517                            // Clear previous status lines
518                            if prev_lines > 0 {
519                                print!("\x1B[{}A\x1B[J", prev_lines);
520                            }
521
522                            // Display current status for each host
523                            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                            // Track how many lines we printed for clearing next time
573                            prev_lines = host_statuses.len() + 2;
574                        }
575                    }
576
577                    // Wait before polling again
578                    tokio::time::sleep(Duration::from_secs(1)).await;
579                }
580            }
581        }
582
583        // STEP 3: Configure network after all hosts are bootstrapped
584        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                // Poll status until network configuration is complete
604                self.wait_for_process_completion(cloud_name, "network")
605                    .await?;
606            }
607        }
608
609        // STEP 4: Set up monitoring if enabled
610        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                    // Poll status until monitoring setup is complete
631                    self.wait_for_process_completion(cloud_name, "monitoring")
632                        .await?;
633                }
634            }
635        }
636
637        // STEP 5: Set up backups if enabled
638        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                    // Poll status until backup setup is complete
659                    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    // Generic helper to wait for process completion by polling the status endpoint
675    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; // 2 minutes with 1-second intervals
683
684        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                    // Check if the overall platform status is completed
699                    if response.status == "completed" {
700                        complete = true;
701                        println!(
702                            "{}",
703                            style(format!("{} setup completed ✓", process_type)).green()
704                        );
705                        break;
706                    }
707
708                    // Extract host statuses to check specific process status
709                    if let Some(data) = response.data {
710                        if let Ok(host_statuses) =
711                            serde_json::from_value::<Vec<HostDeploymentStatus>>(data)
712                        {
713                            // Different processes have different indicators of completion
714                            match process_type {
715                                "network" => {
716                                    // All hosts should have completed network configuration
717                                    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                                    // Show some progress info
732                                    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                                    // Check if all hosts have the metrics-collector service
742                                    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                                    // Show current step from any host that's setting up monitoring
758                                    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                                    // Check if backup manager is running on bastion hosts
774                                    let backups_ready = host_statuses
775                                        .iter()
776                                        .filter(|h| {
777                                            // This is the previous line with error - no longer referencing config
778                                            // Just check if the host has a backup-manager service
779                                            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                                    // Show backup setup step if available
797                                    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                                    // Generic process - just check if all hosts are completed
810                                    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    } // End of function
841
842    // List SSH hosts
843    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        // Get status from API for all hosts
875        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                        // Display services for each host
891                        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    // Display services status from API data
924    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            // Get backup information from one of the bastion hosts if available
975            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                        // In a real implementation, we would extract these dates from service metadata
988                        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    // Restart a service via API
998    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                // Wait for service to restart by polling the host services endpoint
1039                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    // Helper to wait for a service to restart
1050    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                                // Check service status
1077                                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    // View logs for a specific service
1130    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    // Trigger an immediate backup
1202    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                // Wait for backup to complete by polling the status endpoint
1243                self.wait_for_backup_completion(&config.cloud_name).await?;
1244            }
1245        }
1246
1247        Ok(())
1248    }
1249
1250    // Helper to wait for backup completion
1251    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; // 1 minute timeout
1255
1256        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                        // Display backup information if available
1275                        if let Some(data) = response.data {
1276                            if let Ok(backup_info) =
1277                                serde_json::from_value::<serde_json::Value>(data)
1278                            {
1279                                // Extract and display relevant backup information
1280                                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                        // Extract and display backup progress information
1296                        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}