-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathcmds.rs
More file actions
2091 lines (1865 loc) · 69.3 KB
/
Copy pathcmds.rs
File metadata and controls
2091 lines (1865 loc) · 69.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//! Debug Bundle Module
//!
//! This module contains all functionality related to creating debug bundles
//! for troubleshooting managed hosts and Carbide API issues.
use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt::Formatter;
use std::fs::File;
use std::io::Write;
use std::str::FromStr;
use ::rpc::forge::BmcEndpointRequest;
use carbide_uuid::machine::MachineId;
use chrono::{DateTime, Local, NaiveDateTime, NaiveTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use zip::CompressionMethod;
use zip::write::{FileOptions, ZipWriter};
use crate::errors::CarbideCliError::InvalidDateTimeFromUserInput;
use crate::errors::{CarbideCliError, CarbideCliResult};
use crate::managed_host::DebugBundle;
use crate::rpc::ApiClient;
const MAX_BATCH_SIZE: u32 = 5000;
const CARBIDE_API_CONTAINER_NAME: &str = "carbide-api";
const K8S_CONTAINER_NAME_LABEL: &str = "k8s_container_name";
// 🔗 Grafana link generation
#[derive(Serialize)]
struct GrafanaConfig {
datasource: String,
queries: Vec<GrafanaQuery>,
range: GrafanaTimeRange,
}
#[derive(Serialize)]
struct GrafanaQuery {
expr: String,
#[serde(rename = "refId")]
ref_id: String,
}
#[derive(Serialize)]
struct GrafanaTimeRange {
from: String,
to: String,
}
// LogType enum for log categorization
#[derive(Debug, Clone, Copy)]
enum LogType {
CarbideApi,
HostSpecific,
DpuAgent,
}
impl LogType {
fn batch_label(&self, batch_number: usize) -> String {
match self {
LogType::CarbideApi => format!("Carbide-API Batch {batch_number}"),
LogType::HostSpecific => format!("Host Batch {batch_number}"),
LogType::DpuAgent => format!("DPU-Agent Batch {batch_number}"),
}
}
fn as_str(&self) -> &'static str {
match self {
LogType::CarbideApi => "carbide-api",
LogType::HostSpecific => "host-specific",
LogType::DpuAgent => "dpu-agent",
}
}
}
// TimeRange struct to group related time parameters
#[derive(Debug, Copy, Clone)]
struct TimeRange {
start: DateTime<Utc>,
end: DateTime<Utc>,
utc: bool,
}
impl TimeRange {
fn to_grafana_format(self) -> (i64, i64) {
(self.start.timestamp_millis(), self.end.timestamp_millis())
}
fn with_new_start_time(self, new_start_time: DateTime<Utc>) -> Self {
let orig_duration = self.end - self.start;
Self {
start: new_start_time,
end: new_start_time + orig_duration,
utc: self.utc,
}
}
fn display_start(&self) -> DisplayDateTime {
DisplayDateTime {
date_time: self.start,
utc: self.utc,
}
}
// function to format end display timestamp
fn format_end_display(&self, end_ms: i64) -> String {
format!(
"{} ({})",
DisplayDateTime {
date_time: self.end,
utc: self.utc,
},
end_ms
)
}
}
// LogBatch struct for batch management
#[derive(Debug)]
struct LogBatch {
batch_number: usize,
log_type: LogType,
time_range: TimeRange,
grafana_link: Option<String>,
}
impl LogBatch {
fn new(batch_number: usize, log_type: LogType, time_range: TimeRange) -> Self {
Self {
batch_number,
log_type,
time_range,
grafana_link: None,
}
}
fn set_grafana_link(
&mut self,
grafana_base_url: &str,
loki_uid: &str,
expr: &str,
) -> CarbideCliResult<()> {
let (start_ms, end_ms) = self.time_range.to_grafana_format();
let link = generate_grafana_link(grafana_base_url, loki_uid, expr, start_ms, end_ms)?;
self.grafana_link = Some(link);
Ok(())
}
fn label(&self) -> String {
self.log_type.batch_label(self.batch_number)
}
fn needs_pagination(batch_count: usize, batch_size: u32) -> bool {
batch_count >= batch_size as usize
}
fn next_time_range(
previous_time_range: TimeRange,
previous_entry_count: usize,
newest_timestamp: Option<i64>,
batch_size: u32,
) -> CarbideCliResult<Option<TimeRange>> {
if let Some(next_start_time) =
handle_pagination(previous_entry_count, newest_timestamp, batch_size as usize)?
{
Ok(Some(
previous_time_range.with_new_start_time(next_start_time),
))
} else {
Ok(None)
}
}
}
// LogCollector struct to encapsulate state and behavior
#[derive(Debug)]
struct LogCollector<'a> {
grafana_base_url: Cow<'a, str>,
loki_uid: Cow<'a, str>,
unique_log_ids: HashSet<String>,
all_entries: Vec<LogEntry>,
batch_size: u32,
batch_links: Vec<(String, String, usize, String)>, // (batch_label, grafana_link, log_count, time_range_display)
grafana_client: GrafanaClient<'a>, // Reuse client across batches
}
struct DisplayDateTime {
date_time: DateTime<Utc>,
utc: bool,
}
impl std::fmt::Display for DisplayDateTime {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let fmt = "%Y-%m-%d %H:%M:%S%Z";
let s = if self.utc {
self.date_time.format(fmt)
} else {
self.date_time.with_timezone(&Local).format(fmt)
};
write!(f, "{s}")
}
}
impl<'a> LogCollector<'a> {
fn new(
grafana_base_url: Cow<'a, str>,
loki_uid: Cow<'a, str>,
batch_size: u32,
) -> CarbideCliResult<Self> {
// Validate and cap batch size
let capped_batch_size = batch_size.min(MAX_BATCH_SIZE);
if batch_size > MAX_BATCH_SIZE {
println!(
" WARNING: Batch size {batch_size} exceeds maximum {MAX_BATCH_SIZE}, using {capped_batch_size}"
);
}
// Create GrafanaClient once and reuse
let grafana_client = GrafanaClient::new(grafana_base_url.clone())?;
Ok(Self {
grafana_base_url,
loki_uid,
unique_log_ids: HashSet::new(),
all_entries: Vec::new(),
batch_size: capped_batch_size,
batch_links: Vec::new(),
grafana_client,
})
}
async fn into_logs_and_batch_links(
mut self,
expr: &str,
log_type: LogType,
mut time_range: TimeRange,
) -> CarbideCliResult<(Vec<LogEntry>, Vec<(String, String, usize, String)>)> {
let mut batch_number = 1;
loop {
let mut batch = LogBatch::new(batch_number, log_type, time_range);
let (start_ms, end_ms) = batch.time_range.to_grafana_format();
let end_display = batch.time_range.format_end_display(end_ms);
println!(
" {}: Fetching logs from {} ({}) to {}",
batch.label(),
batch.time_range.display_start(),
start_ms,
end_display
);
let batch_result = self.process_batch(expr, start_ms, end_ms).await?;
// Generate Grafana link for this batch
batch.set_grafana_link(&self.grafana_base_url, &self.loki_uid, expr)?;
// Store batch info with link and time range
let batch_label = batch.label();
let grafana_link = batch.grafana_link.unwrap_or_default();
let log_count = batch_result.entries.len();
let time_range_display = format!(
"{} ({}) to {}",
batch.time_range.start, start_ms, end_display
);
self.batch_links
.push((batch_label, grafana_link, log_count, time_range_display));
// Update collections for next batch
self.unique_log_ids.extend(
batch_result
.entries
.iter()
.map(|entry| entry.unique_id.clone()),
);
self.all_entries.extend(batch_result.entries);
if !LogBatch::needs_pagination(batch_result.original_batch_count, self.batch_size) {
break;
}
if let Some(next_time_range) = LogBatch::next_time_range(
batch.time_range,
log_count,
batch_result.newest_timestamp,
self.batch_size,
)? {
time_range = next_time_range;
batch_number += 1;
} else {
break;
}
}
self.finalize_and_validate_logs(&log_type)?;
Ok((self.all_entries, self.batch_links))
}
async fn process_batch(
&self,
expr: &str,
start_ms: i64,
end_ms: i64,
) -> CarbideCliResult<BatchResult> {
let query_request =
build_grafana_query_request(expr, start_ms, end_ms, &self.loki_uid, self.batch_size);
// 2. Execute HTTP request using reusable function and stored client
let response_body = execute_grafana_query(&query_request, &self.grafana_client).await?;
// 3. Parse response using reusable function
let (batch_entries, newest_timestamp) = parse_grafana_logs(response_body)?;
let original_batch_count = batch_entries.len();
let new_entries = remove_duplicates_from_end(batch_entries, &self.unique_log_ids);
Ok(BatchResult {
entries: new_entries,
newest_timestamp,
original_batch_count,
})
}
fn finalize_and_validate_logs(&self, log_type: &LogType) -> CarbideCliResult<()> {
let log_type_upper = log_type.as_str().to_uppercase();
println!(
" TOTAL {} LOGS COLLECTED: {}",
log_type_upper,
self.all_entries.len()
);
let logs_count = self.all_entries.len();
let unique_ids_count = self.unique_log_ids.len();
if logs_count != unique_ids_count {
println!(
" Validation FAILED for {}: {} logs but {} unique IDs (some logs missing unique IDs)",
log_type.as_str(),
logs_count,
unique_ids_count
);
return Err(CarbideCliError::GenericError(format!(
"Log validation failed for {}: {logs_count} logs but {unique_ids_count} unique IDs",
log_type.as_str()
)));
}
println!(
" Validation PASSED for {}: {} logs = {} unique IDs",
log_type.as_str(),
logs_count,
unique_ids_count
);
Ok(())
}
}
// GrafanaClient struct for API interactions
#[derive(Debug)]
struct GrafanaClient<'a> {
client: reqwest::Client,
base_url: Cow<'a, str>,
auth_token: String,
}
impl<'a> GrafanaClient<'a> {
fn new(grafana_url: Cow<'a, str>) -> CarbideCliResult<Self> {
let auth_token = std::env::var("GRAFANA_AUTH_TOKEN")
.map_err(|_| CarbideCliError::GenericError(
"GRAFANA_AUTH_TOKEN environment variable not set. Please set it with your Grafana bearer token.".to_string()
))?;
// Build HTTP client with optional proxy support from environment variables
let mut client_builder = reqwest::Client::builder();
// Check for proxy configuration in environment variables
// Standard proxy env vars: HTTPS_PROXY, https_proxy, HTTP_PROXY, http_proxy
if let Ok(proxy_url) = std::env::var("HTTPS_PROXY")
.or_else(|_| std::env::var("https_proxy"))
.or_else(|_| std::env::var("HTTP_PROXY"))
.or_else(|_| std::env::var("http_proxy"))
{
println!(" Using proxy: {}", proxy_url);
let proxy = reqwest::Proxy::all(&proxy_url).map_err(|e| {
CarbideCliError::GenericError(format!("Failed to configure proxy: {}", e))
})?;
client_builder = client_builder.proxy(proxy);
} else {
println!(" No proxy configured - connecting directly");
}
let client = client_builder.build().map_err(|e| {
CarbideCliError::GenericError(format!("Failed to build HTTP client: {}", e))
})?;
Ok(Self {
client,
base_url: grafana_url,
auth_token,
})
}
async fn get_loki_datasource_uid(&self) -> CarbideCliResult<String> {
println!(
" Fetching Loki datasource UID from Grafana: {}",
self.base_url
);
let datasources_url = format!("{}/api/datasources/", self.base_url);
let response = self
.client
.get(&datasources_url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", format!("Bearer {}", self.auth_token))
.send()
.await;
match response {
Ok(resp) => {
let status = resp.status();
println!(" Datasources API Response Status: {status}");
if status.is_success() {
let datasources: Vec<GrafanaDatasource> = match resp.json().await {
Ok(data) => data,
Err(e) => {
return Err(CarbideCliError::GenericError(format!(
"Failed to parse datasources JSON: {e}"
)));
}
};
for ds in datasources {
if ds.datasource_type == "loki" {
println!(" Found Loki datasource: {} (UID: {})", ds.name, ds.uid);
return Ok(ds.uid);
}
}
Err(CarbideCliError::GenericError(
"Loki datasource not found in the response".to_string(),
))
} else {
let body = resp.text().await.unwrap_or_default();
Err(CarbideCliError::GenericError(format!(
"HTTP Error {status}: {body}"
)))
}
}
Err(e) => Err(CarbideCliError::GenericError(format!(
"Failed to fetch datasources: {e}"
))),
}
}
}
// LogEntry struct for log entries
#[derive(Debug, Clone)]
struct LogEntry {
message: String,
timestamp_ms: i64,
unique_id: String,
nanosecond_timestamp: u64,
}
impl LogEntry {
fn format_header(&self) -> String {
format_timestamp_header(self.timestamp_ms)
}
fn is_duplicate(&self, existing_ids: &std::collections::HashSet<String>) -> bool {
existing_ids.contains(&self.unique_id)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GrafanaResponse {
pub results: GrafanaResults,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GrafanaResults {
#[serde(rename = "A")]
pub a: GrafanaFrameResult,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GrafanaFrameResult {
pub status: u16,
pub frames: Vec<GrafanaFrame>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GrafanaFrame {
pub data: GrafanaFrameData,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GrafanaFrameData {
pub values: Vec<Vec<GrafanaValue>>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum GrafanaValue {
Int(i64), // For timestamps (values[1])
String(String), // For log messages (values[2]) and nanosecond timestamps (values[3])
Object(serde_json::Value),
}
// Strongly typed structs for Grafana query requests
#[derive(Serialize)]
struct GrafanaQueryRequest {
queries: Vec<LokiQuery>,
from: String,
to: String,
limit: u32,
}
#[derive(Serialize)]
struct LokiQuery {
#[serde(rename = "refId")]
ref_id: String,
datasource: LokiDatasource,
#[serde(rename = "queryType")]
query_type: String,
expr: String,
#[serde(rename = "maxLines")]
max_lines: u32,
}
#[derive(Serialize)]
struct LokiDatasource {
#[serde(rename = "type")]
datasource_type: String,
uid: String,
}
// Grafana Datasource API Response Structs
#[derive(Deserialize, Debug)]
struct GrafanaDatasource {
pub uid: String,
pub name: String,
#[serde(rename = "type")]
pub datasource_type: String,
}
// Site Controller Details - Holds BMC endpoint exploration data
struct SiteControllerAnalysis {
exploration_report: ::rpc::site_explorer::EndpointExplorationReport,
credential_status: ::rpc::forge::BmcCredentialStatusResponse,
bmc_ip: String,
bmc_mac: Option<String>,
}
// Machine Info - Holds machine state machine data
struct MachineAnalysis {
machine: ::rpc::forge::Machine,
validation_results: Vec<::rpc::forge::MachineValidationResult>,
}
/// Helper function to get BMC IP and MAC address from machine_id
#[allow(deprecated)]
async fn get_bmc_ip_from_host_id(
api_client: &ApiClient,
host_id: &str,
) -> CarbideCliResult<(String, Option<String>)> {
// Parse machine ID
let machine_id = MachineId::from_str(host_id).map_err(|e| {
CarbideCliError::GenericError(format!("Invalid machine ID '{}': {}", host_id, e))
})?;
// Get machine details from API
let machine = api_client.get_machine(machine_id).await?;
// Extract BMC info
let bmc_info = machine.bmc_info.ok_or_else(|| {
CarbideCliError::GenericError(format!(
"Machine {} does not have BMC info available",
host_id
))
})?;
// Extract BMC IP (required)
let bmc_ip = bmc_info.ip.ok_or_else(|| {
CarbideCliError::GenericError(format!(
"Machine {} does not have BMC IP address available",
host_id
))
})?;
// Extract BMC MAC (optional)
let bmc_mac = bmc_info.mac;
Ok((bmc_ip, bmc_mac))
}
/// Fetch Site Controller Details (Redfish exploration + credentials)
async fn get_site_controller_analysis(
api_client: &ApiClient,
host_id: &str,
) -> CarbideCliResult<SiteControllerAnalysis> {
println!(" Fetching BMC information for machine {}...", host_id);
// Step 1: Get BMC IP and MAC from machine_id
let (bmc_ip, bmc_mac) = get_bmc_ip_from_host_id(api_client, host_id).await?;
println!(" BMC IP: {}", bmc_ip);
if let Some(ref mac) = bmc_mac {
println!(" BMC MAC: {}", mac);
}
// Parse MAC address if available
let mac_address = if let Some(ref mac_str) = bmc_mac {
use mac_address::MacAddress;
Some(MacAddress::from_str(mac_str).map_err(|e| {
CarbideCliError::GenericError(format!("Invalid MAC address '{}': {:?}", mac_str, e))
})?)
} else {
None
};
println!(" Exploring BMC endpoint via Redfish...");
// Step 2: Call Explore RPC (fetches Redfish data)
let exploration_report = api_client
.0
.explore(BmcEndpointRequest {
ip_address: bmc_ip.clone(),
mac_address: mac_address.map(|m| m.to_string()),
})
.await?;
println!(" Systems: {} found", exploration_report.systems.len());
println!(" Managers: {} found", exploration_report.managers.len());
println!(" Chassis: {} found", exploration_report.chassis.len());
// Step 3: Call BmcCredentialStatus RPC
let credential_status = api_client
.0
.bmc_credential_status(BmcEndpointRequest {
ip_address: bmc_ip.clone(),
mac_address: mac_address.map(|m| m.to_string()),
})
.await?;
println!(
" Credentials: Available = {}",
credential_status.have_credentials
);
Ok(SiteControllerAnalysis {
exploration_report,
credential_status,
bmc_ip,
bmc_mac,
})
}
/// Fetch machine info (state machine information)
async fn get_machine_analysis(
api_client: &ApiClient,
machine_id: &MachineId,
) -> CarbideCliResult<MachineAnalysis> {
println!(" Fetching machine state and metadata...");
// Get machine details (state, SLA, controller outcome, reboot info, errors)
let machine = api_client.get_machine(*machine_id).await?;
println!(" Current State: {}", machine.state);
// Get validation results
println!(" Fetching validation test failures...");
let validation_list = api_client
.get_machine_validation_results(Some(*machine_id), true, None)
.await?;
// Filter: Keep ONLY failed tests (exit_code != 0)
let failed_tests: Vec<_> = validation_list
.results
.into_iter()
.filter(|test| test.exit_code != 0)
.collect();
println!(
" Validation Failures: {} failed tests found",
failed_tests.len()
);
Ok(MachineAnalysis {
machine,
validation_results: failed_tests,
})
}
/// Creates a comprehensive debug bundle for a specific machine.
///
/// This function collects diagnostic information from multiple sources and packages
/// them into a ZIP file for debugging and troubleshooting purposes.
///
/// # Data Collected
///
/// The debug bundle includes the following components:
///
/// 1. **Host-Specific Logs**: Machine-specific logs from Loki (filtered by `host_machine_id`)
/// 2. **Carbide-API Logs**: API server logs from Loki (filtered by `k8s_container_name`)
/// 3. **DPU Agent Logs**: DPU agent service logs from Loki (filtered by `systemd_unit` and `host_machine_id`)
/// 4. **Health Alerts**: Historical health alerts for the machine within the specified time range
/// 5. **Health Report Entries**: Current health report entries configured for the machine
/// 6. **Site Controller Details**: BMC/Redfish exploration data including:
/// - BMC IP and MAC addresses
/// - Systems, Managers, and Chassis information
/// - Firmware inventory
/// - Credential availability status
/// 7. **Machine Info**: State machine information including:
/// - Current state and state version
/// - SLA status and controller outcome
/// - Validation test failures
/// - Reboot history and failure details
/// 8. **Metadata**: Summary file with batch information and Grafana links
///
/// # Arguments
///
/// * `debug_bundle` - Configuration containing:
/// - `host_id`: The machine ID to collect data for
/// - `start_time`/`end_time`: Time range for log collection (HH:MM:SS format)
/// - `output_path`: Directory where the ZIP file will be created
/// - `site`: Site name (e.g., "dev3", "prod")
/// - `batch_size`: Maximum logs per batch (default: 5000)
///
/// * `api_client` - Authenticated API client for making RPC calls to Carbide API
///
/// # Output
///
/// Creates a ZIP file with the following structure:
/// - `host_logs_<machine_id>.txt` - Host-specific logs
/// - `carbide_api_logs.txt` - API server logs
/// - `dpu_agent_logs_<machine_id>.txt` - DPU agent service logs
/// - `health_alerts.json` - Health alerts history
/// - `health_alert_overrides.json` - Active health report entries
/// - `site_controller_details.json` - BMC/Redfish exploration data
/// - `machine_info.json` - Machine state and validation data
/// - `metadata.txt` - Summary and Grafana links
///
/// # Returns
///
/// Returns `Ok(())` on successful bundle creation, or a `CarbideCliError` if any step fails.
///
/// # Example
///
/// ```no_run
/// use crate::managed_host::DebugBundle;
/// use crate::rpc::ApiClient;
///
/// let bundle_config = DebugBundle {
/// host_id: "fm100ht...".to_string(),
/// start_time: "06:00:00".to_string(),
/// end_time: Some("06:10:00".to_string()),
/// utc: false,
/// output_path: "/tmp".to_string(),
/// grafana_url: Some("https://grafana.example.com".to_string()),
/// batch_size: 5000,
/// };
///
/// let api_client = ApiClient::new(config).await?;
/// handle_debug_bundle(bundle_config, &api_client).await?;
/// ```
pub async fn handle_debug_bundle(
debug_bundle: DebugBundle,
api_client: &ApiClient,
) -> CarbideCliResult<()> {
println!(
" Creating debug bundle for host: {}",
debug_bundle.host_id
);
// Parse flexible date/time inputs
let start = parse_datetime_input(&debug_bundle.start_time, debug_bundle.utc)?;
// Handle optional end_time (default to "now")
let end = if let Some(ref end_time_str) = debug_bundle.end_time {
parse_datetime_input(end_time_str, debug_bundle.utc)?
} else {
// Use current time as default
chrono::Utc::now()
};
// Create TimeRange struct with parsed values
let time_range = TimeRange {
start,
end,
utc: debug_bundle.utc,
};
// Conditionally collect logs based on --grafana-url presence
let (
host_logs,
host_batch_links,
carbide_api_logs,
carbide_batch_links,
dpu_agent_logs,
dpu_batch_links,
loki_uid,
) = if let Some(grafana_url) = &debug_bundle.grafana_url {
// Use new GrafanaClient struct
let grafana_client = GrafanaClient::new(Cow::Borrowed(grafana_url))?;
println!("\nFetching Loki datasource UID...");
let loki_uid = grafana_client.get_loki_datasource_uid().await?;
println!("\nDownloading host-specific logs...");
let (host_logs, host_batch_links) = get_host_logs(
&debug_bundle.host_id,
time_range,
grafana_url,
&loki_uid,
debug_bundle.batch_size,
)
.await?;
println!("\nDownloading carbide-api logs...");
let (carbide_api_logs, carbide_batch_links) =
get_carbide_api_logs(time_range, grafana_url, &loki_uid, debug_bundle.batch_size)
.await?;
println!("\nDownloading DPU agent logs...");
let (dpu_agent_logs, dpu_batch_links) = get_dpu_agent_logs(
&debug_bundle.host_id,
time_range,
grafana_url,
&loki_uid,
debug_bundle.batch_size,
)
.await?;
(
host_logs,
host_batch_links,
carbide_api_logs,
carbide_batch_links,
dpu_agent_logs,
dpu_batch_links,
Some(loki_uid),
)
} else {
println!("\nSkipping log collection (--grafana-url not provided)");
(
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
None,
)
};
println!("\nFetching health alerts...");
let health_alerts = get_health_alerts(api_client, &debug_bundle.host_id, &time_range).await?;
let alert_count = health_alerts
.histories
.get(&debug_bundle.host_id)
.map(|h| h.records.len())
.unwrap_or(0);
println!(" Alerts: {} records collected", alert_count);
println!("\nFetching health report entries...");
let alert_entries = get_alert_entries(api_client, &debug_bundle.host_id).await?;
println!(
" Entries: {} entries collected",
alert_entries.health_report_entries.len()
);
println!("\nFetching site controller details...");
let site_controller_analysis =
get_site_controller_analysis(api_client, &debug_bundle.host_id).await?;
// Machine Info
println!("\nFetching machine info...");
let machine_id = MachineId::from_str(&debug_bundle.host_id).map_err(|e| {
CarbideCliError::GenericError(format!(
"Invalid machine ID '{}': {}",
debug_bundle.host_id, e
))
})?;
let machine_analysis = get_machine_analysis(api_client, &machine_id).await?;
println!("\nDebug Bundle Summary:");
println!(" Host Logs: {} logs collected", host_logs.len());
println!(
" Carbide-API Logs: {} logs collected",
carbide_api_logs.len()
);
println!(" DPU Agent Logs: {} logs collected", dpu_agent_logs.len());
println!(
" Health Alerts: {} records",
health_alerts
.histories
.get(&debug_bundle.host_id)
.map(|h| h.records.len())
.unwrap_or(0)
);
println!(
" Health Report Entries: {} entries",
alert_entries.health_report_entries.len()
);
println!(" Site Controller Details: Collected");
println!(" Machine State Information: Collected");
println!(
" Total Logs: {}",
host_logs.len() + carbide_api_logs.len() + dpu_agent_logs.len()
);
// Create ZIP file with logs, health alerts, health report entries, site controller details, and machine info
println!("\nCreating ZIP file...");
create_debug_bundle_zip(
&debug_bundle,
&host_logs,
&carbide_api_logs,
&dpu_agent_logs,
&host_batch_links,
&carbide_batch_links,
&dpu_batch_links,
loki_uid.as_deref(),
&health_alerts,
&alert_entries,
&site_controller_analysis,
&machine_analysis,
)?;
println!("\nDebug bundle creation completed!");
Ok(())
}
async fn get_host_logs(
host_id: &str,
time_range: TimeRange,
grafana_url: &str,
loki_uid: &str,
batch_size: u32,
) -> CarbideCliResult<(Vec<LogEntry>, Vec<(String, String, usize, String)>)> {
let expr = format!("{{host_machine_id=\"{host_id}\"}} |= ``");
let log_type = LogType::HostSpecific;
// NEW() NOW RETURNS RESULT
let collector = LogCollector::new(grafana_url.into(), loki_uid.into(), batch_size)?;
collector
.into_logs_and_batch_links(&expr, log_type, time_range)
.await
}
async fn get_carbide_api_logs(
time_range: TimeRange,
grafana_url: &str,
loki_uid: &str,
batch_size: u32,
) -> CarbideCliResult<(Vec<LogEntry>, Vec<(String, String, usize, String)>)> {
let expr = format!("{{{K8S_CONTAINER_NAME_LABEL}=\"{CARBIDE_API_CONTAINER_NAME}\"}} |= ``");
let log_type = LogType::CarbideApi;
// NEW() NOW RETURNS RESULT
let collector = LogCollector::new(grafana_url.into(), loki_uid.into(), batch_size)?;
collector
.into_logs_and_batch_links(&expr, log_type, time_range)
.await
}
async fn get_dpu_agent_logs(
host_id: &str,
time_range: TimeRange,
grafana_url: &str,
loki_uid: &str,
batch_size: u32,
) -> CarbideCliResult<(Vec<LogEntry>, Vec<(String, String, usize, String)>)> {
let expr = format!(
"{{systemd_unit=\"forge-dpu-agent.service\", host_machine_id=\"{host_id}\"}} |= ``"
);
let log_type = LogType::DpuAgent;
let collector = LogCollector::new(grafana_url.into(), loki_uid.into(), batch_size)?;